okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petrequire OTP 29: pin erlang_29 in the flake and drop the zstd availability fallbacksc7169a0

main
6.7 KiB199 linesraw
1//go:build unix
2
3package e2e
4
5import (
6	"bufio"
7	"bytes"
8	"context"
9	"encoding/base64"
10	"encoding/json"
11	"io"
12	"net/http"
13	"strings"
14	"sync"
15	"testing"
16	"time"
17
18	"albedo/cli/internal/daemon"
19	"github.com/klauspost/compress/zstd"
20)
21
22// uploadRecorder keeps the Content-Encoding of each input upload.
23type uploadRecorder struct {
24	upstream  http.RoundTripper
25	mu        sync.Mutex
26	encodings []string
27}
28
29func (recorder *uploadRecorder) RoundTrip(req *http.Request) (*http.Response, error) {
30	if req.Method == http.MethodPut && strings.Contains(req.URL.Path, "/inputs/") {
31		recorder.mu.Lock()
32		recorder.encodings = append(recorder.encodings, req.Header.Get("Content-Encoding"))
33		recorder.mu.Unlock()
34	}
35	return recorder.upstream.RoundTrip(req)
36}
37
38// rawGet reads a daemon resource without the CLI's transport, so the test
39// sees the coding the daemon chose.
40func rawGet(t *testing.T, path string, headers map[string]string) *http.Response {
41	t.Helper()
42	req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, conn(t).BaseURL()+path, nil)
43	if err != nil {
44		t.Fatal(err)
45	}
46	req.Header.Set("Authorization", "Bearer "+conn(t).Snapshot().Token)
47	for name, value := range headers {
48		req.Header.Set(name, value)
49	}
50	res, err := (&http.Client{Transport: &http.Transport{DisableCompression: true}}).Do(req)
51	if err != nil {
52		t.Fatal(err)
53	}
54	t.Cleanup(func() { _ = res.Body.Close() })
55	return res
56}
57
58func decoded(t *testing.T, res *http.Response) []byte {
59	t.Helper()
60	var body io.Reader = res.Body
61	if res.Header.Get("Content-Encoding") == "zstd" {
62		decoder, err := zstd.NewReader(res.Body)
63		if err != nil {
64			t.Fatal(err)
65		}
66		defer decoder.Close()
67		body = decoder
68	}
69	data, err := io.ReadAll(body)
70	if err != nil {
71		t.Fatal(err)
72	}
73	return data
74}
75
76// A large upload crosses zstd-coded once the daemon advertises it, the reply
77// and its history come back coded and identical to identity, the session
78// stream decodes frame by frame, and the attached image is served as itself.
79func TestZstdCodingAndRawImages(t *testing.T) {
80	profile := providerRoute(t, echoReply)
81	t.Parallel()
82	attached, err := daemon.Attach(t.Context(), conn(t).Snapshot(), nil)
83	if err != nil {
84		t.Fatal(err)
85	}
86	t.Cleanup(attached.HTTPClient().CloseIdleConnections)
87	recorder := &uploadRecorder{upstream: attached.HTTPClient().Transport}
88	attached.HTTPClient().Transport = recorder
89
90	id := newSession(t, t.TempDir())
91	png := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"
92	prompt := strings.Repeat("a long prompt that compresses well. ", 600)
93	image := daemon.ImageAttachment{Data: png, ImageMetadata: daemon.ImageMetadata{MimeType: daemon.ImagePNG, Width: 1, Height: 1, Bytes: 69}}
94	if _, err := daemon.NewChatClient(attached, id).Send(context.Background(), prompt, []daemon.ImageAttachment{image}); err != nil {
95		t.Fatalf("compressed upload: %v", err)
96	}
97	waitIdle(t, id, profile, 1)
98	if recorder.encodings[0] != "zstd" {
99		t.Fatalf("a %d byte upload went out as %q", len(prompt), recorder.encodings[0])
100	}
101	if forwarded := lastUserText(suite.provider.requests(profile)[0]["body"].(map[string]any)); !strings.Contains(forwarded, prompt) {
102		t.Fatal("the decoded upload lost the prompt")
103	}
104
105	path := "/sessions/" + id + "/history?limit=200"
106	coded := rawGet(t, path, map[string]string{"Accept-Encoding": "gzip, zstd"})
107	plain := rawGet(t, path, nil)
108	if coded.Header.Get("Content-Encoding") != "zstd" || !strings.Contains(coded.Header.Get("Vary"), "Accept-Encoding") || plain.Header.Get("Content-Encoding") != "" {
109		t.Fatalf("history coding: %q vary %q, identity %q", coded.Header.Get("Content-Encoding"), coded.Header.Get("Vary"), plain.Header.Get("Content-Encoding"))
110	}
111	history := decoded(t, coded)
112	if !bytes.Equal(history, decoded(t, plain)) {
113		t.Fatal("the zstd history differs from identity")
114	}
115	refused := rawGet(t, path, map[string]string{"Accept-Encoding": "zstd;q=0"})
116	if refused.Header.Get("Content-Encoding") != "" {
117		t.Fatal("zstd;q=0 still got zstd")
118	}
119
120	var page struct {
121		Items []struct {
122			Content []struct {
123				Image *struct {
124					Reference struct{ URL string } `json:"reference"`
125				} `json:"image"`
126			} `json:"content"`
127		} `json:"items"`
128	}
129	if err := json.Unmarshal(history, &page); err != nil {
130		t.Fatal(err)
131	}
132	var url string
133	for _, item := range page.Items {
134		for _, part := range item.Content {
135			if part.Image != nil {
136				url = part.Image.Reference.URL
137			}
138		}
139	}
140	served := rawGet(t, url, map[string]string{"Accept-Encoding": "zstd"})
141	want, _ := base64.StdEncoding.DecodeString(png)
142	if got, _ := io.ReadAll(served.Body); served.StatusCode != http.StatusOK || served.Header.Get("Content-Type") != "image/png" || served.Header.Get("Content-Encoding") != "" || !bytes.Equal(got, want) {
143		t.Fatalf("image %s: %d %q %q, %d bytes", url, served.StatusCode, served.Header.Get("Content-Type"), served.Header.Get("Content-Encoding"), len(got))
144	}
145
146	stream := rawGet(t, "/sessions/"+id, map[string]string{"Accept": "text/event-stream", "Accept-Encoding": "zstd"})
147	if stream.Header.Get("Content-Encoding") != "zstd" {
148		t.Fatal("the session stream was not zstd-coded")
149	}
150	frames, err := zstd.NewReader(stream.Body, zstd.WithDecoderConcurrency(1))
151	if err != nil {
152		t.Fatal(err)
153	}
154	defer frames.Close()
155	first := make(chan string, 1)
156	go func() {
157		line, _ := bufio.NewReaderSize(frames, 1<<20).ReadString('\n')
158		first <- line
159	}()
160	select {
161	case line := <-first:
162		if !strings.HasPrefix(line, "data: {") {
163			t.Fatalf("first stream frame: %q", line)
164		}
165	case <-time.After(10 * time.Second):
166		t.Fatal("the coded stream withheld its first frame")
167	}
168}
169
170// A coded body must be one frame that declares a size within the route's
171// limit; anything else is refused before the operation runs.
172func TestZstdRequestBodiesAreBounded(t *testing.T) {
173	t.Parallel()
174	unsized, _ := zstd.NewWriter(nil)
175	var streamed bytes.Buffer
176	unsized.Reset(&streamed)
177	_, _ = unsized.Write([]byte(`{"name":"x"}`))
178	_ = unsized.Close()
179	bomb, _ := zstd.NewWriter(nil)
180	for name, body := range map[string][]byte{
181		"garbage":   []byte("not zstd"),
182		"unsized":   streamed.Bytes(),
183		"oversized": bomb.EncodeAll(bytes.Repeat([]byte{' '}, 1<<20), nil),
184	} {
185		req, _ := http.NewRequestWithContext(t.Context(), http.MethodPost, conn(t).BaseURL()+"/hosts/coded/probe", bytes.NewReader(body))
186		req.Header.Set("Authorization", "Bearer "+conn(t).Snapshot().Token)
187		req.Header.Set("Content-Type", "application/json")
188		req.Header.Set("Content-Encoding", "zstd")
189		res, err := http.DefaultClient.Do(req)
190		if err != nil {
191			t.Fatal(err)
192		}
193		problem, _ := io.ReadAll(res.Body)
194		_ = res.Body.Close()
195		if res.StatusCode != http.StatusBadRequest || !strings.Contains(string(problem), "does not decode") {
196			t.Errorf("%s body: %d %s", name, res.StatusCode, problem)
197		}
198	}
199}