okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
c7169a0
main
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 { 24upstream http.RoundTripper 25mu sync.Mutex 26encodings []string 27} 28 29func (recorder * uploadRecorder )RoundTrip (req * http.Request ) (* http.Response ,error ) { 30if req .Method == http .MethodPut && strings .Contains (req .URL .Path ,"/inputs/" ) { 31recorder .mu .Lock () 32recorder .encodings = append (recorder .encodings ,req .Header .Get ("Content-Encoding" )) 33recorder .mu .Unlock () 34 } 35return 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 { 41t .Helper () 42req ,err := http .NewRequestWithContext (t .Context (),http .MethodGet ,conn (t ).BaseURL ()+ path ,nil ) 43if err != nil { 44t .Fatal (err ) 45 } 46req .Header .Set ("Authorization" ,"Bearer " + conn (t ).Snapshot ().Token ) 47for name ,value := range headers { 48req .Header .Set (name ,value ) 49 } 50res ,err := (& http.Client {Transport :& http.Transport {DisableCompression :true }}).Do (req ) 51if err != nil { 52t .Fatal (err ) 53 } 54t .Cleanup (func () {_ = res .Body .Close () }) 55return res 56} 57 58func decoded (t * testing.T ,res * http.Response ) []byte { 59t .Helper () 60var body io.Reader = res .Body 61if res .Header .Get ("Content-Encoding" )== "zstd" { 62decoder ,err := zstd .NewReader (res .Body ) 63if err != nil { 64t .Fatal (err ) 65 } 66defer decoder .Close () 67body = decoder 68 } 69data ,err := io .ReadAll (body ) 70if err != nil { 71t .Fatal (err ) 72 } 73return 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 ) { 80profile := providerRoute (t ,echoReply ) 81t .Parallel () 82attached ,err := daemon .Attach (t .Context (),conn (t ).Snapshot (),nil ) 83if err != nil { 84t .Fatal (err ) 85 } 86t .Cleanup (attached .HTTPClient ().CloseIdleConnections ) 87recorder := & uploadRecorder {upstream :attached .HTTPClient ().Transport } 88attached .HTTPClient ().Transport = recorder 89 90id := newSession (t ,t .TempDir ()) 91png := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC" 92prompt := strings .Repeat ("a long prompt that compresses well. " ,600 ) 93image := daemon.ImageAttachment {Data :png ,ImageMetadata : daemon.ImageMetadata {MimeType :daemon .ImagePNG ,Width :1 ,Height :1 ,Bytes :69 }} 94if _ ,err := daemon .NewChatClient (attached ,id ).Send (context .Background (),prompt , []daemon.ImageAttachment {image });err != nil { 95t .Fatalf ("compressed upload: %v" ,err ) 96 } 97waitIdle (t ,id ,profile ,1 ) 98if recorder .encodings [0 ]!= "zstd" { 99t .Fatalf ("a %d byte upload went out as %q" ,len (prompt ),recorder .encodings [0 ]) 100 } 101if forwarded := lastUserText (suite .provider .requests (profile )[0 ]["body" ].(map [string ]any ));! strings .Contains (forwarded ,prompt ) { 102t .Fatal ("the decoded upload lost the prompt" ) 103 } 104 105path := "/sessions/" + id + "/history?limit=200" 106coded := rawGet (t ,path ,map [string ]string {"Accept-Encoding" :"gzip, zstd" }) 107plain := rawGet (t ,path ,nil ) 108if coded .Header .Get ("Content-Encoding" )!= "zstd" || ! strings .Contains (coded .Header .Get ("Vary" ),"Accept-Encoding" )|| plain .Header .Get ("Content-Encoding" )!= "" { 109t .Fatalf ("history coding: %q vary %q, identity %q" ,coded .Header .Get ("Content-Encoding" ),coded .Header .Get ("Vary" ),plain .Header .Get ("Content-Encoding" )) 110 } 111history := decoded (t ,coded ) 112if ! bytes .Equal (history ,decoded (t ,plain )) { 113t .Fatal ("the zstd history differs from identity" ) 114 } 115refused := rawGet (t ,path ,map [string ]string {"Accept-Encoding" :"zstd;q=0" }) 116if refused .Header .Get ("Content-Encoding" )!= "" { 117t .Fatal ("zstd;q=0 still got zstd" ) 118 } 119 120var page struct { 121Items []struct { 122Content []struct { 123Image * struct { 124Reference struct {URL string }`json:"reference"` 125 }`json:"image"` 126 }`json:"content"` 127 }`json:"items"` 128 } 129if err := json .Unmarshal (history ,& page );err != nil { 130t .Fatal (err ) 131 } 132var url string 133for _ ,item := range page .Items { 134for _ ,part := range item .Content { 135if part .Image != nil { 136url = part .Image .Reference .URL 137 } 138 } 139 } 140served := rawGet (t ,url ,map [string ]string {"Accept-Encoding" :"zstd" }) 141want ,_ := base64 .StdEncoding .DecodeString (png ) 142if 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 ) { 143t .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 146stream := rawGet (t ,"/sessions/" + id ,map [string ]string {"Accept" :"text/event-stream" ,"Accept-Encoding" :"zstd" }) 147if stream .Header .Get ("Content-Encoding" )!= "zstd" { 148t .Fatal ("the session stream was not zstd-coded" ) 149 } 150frames ,err := zstd .NewReader (stream .Body ,zstd .WithDecoderConcurrency (1 )) 151if err != nil { 152t .Fatal (err ) 153 } 154defer frames .Close () 155first := make (chan string ,1 ) 156go func () { 157line ,_ := bufio .NewReaderSize (frames ,1 << 20 ).ReadString ('\n' ) 158first <- line 159 }() 160select { 161case line := <- first : 162if ! strings .HasPrefix (line ,"data: {" ) { 163t .Fatalf ("first stream frame: %q" ,line ) 164 } 165case <- time .After (10 * time .Second ): 166t .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 ) { 173t .Parallel () 174unsized ,_ := zstd .NewWriter (nil ) 175var streamed bytes.Buffer 176unsized .Reset (& streamed ) 177_ ,_ = unsized .Write ([]byte (`{"name":"x"}` )) 178_ = unsized .Close () 179bomb ,_ := zstd .NewWriter (nil ) 180for 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 } { 185req ,_ := http .NewRequestWithContext (t .Context (),http .MethodPost ,conn (t ).BaseURL ()+ "/hosts/coded/probe" ,bytes .NewReader (body )) 186req .Header .Set ("Authorization" ,"Bearer " + conn (t ).Snapshot ().Token ) 187req .Header .Set ("Content-Type" ,"application/json" ) 188req .Header .Set ("Content-Encoding" ,"zstd" ) 189res ,err := http .DefaultClient .Do (req ) 190if err != nil { 191t .Fatal (err ) 192 } 193problem ,_ := io .ReadAll (res .Body ) 194_ = res .Body .Close () 195if res .StatusCode != http .StatusBadRequest || ! strings .Contains (string (problem ),"does not decode" ) { 196t .Errorf ("%s body: %d %s" ,name ,res .StatusCode ,problem ) 197 } 198 } 199}