okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
a229862
main
1// Malformed server and refresh races need controlled HTTP peers. The real 2// daemon E2E verifies attachment and lifecycle behavior with valid server. 3package daemon 4 5import ( 6"context" 7"encoding/json" 8"errors" 9"fmt" 10"net" 11"net/http" 12"net/http/httptest" 13"sync/atomic" 14"testing" 15"time" 16) 17 18const readyServer = `{"instance_id":"instance-a","protocol":3,"state":"ready","capabilities":{"durable_inputs":1,"session_replay":2,"collection_invalidation":1,"tool_progress":1},"build":"verified-build","digest":"verified-digest","extensions":[],"quota":[],"notices":[]}` 19 20func TestAttachValidatesServerIdentityAndRequiredCapabilities (t * testing.T ) { 21for _ ,change := range []struct { 22name ,key string 23value any 24compatibility bool 25 }{ 26 {"missing identity" ,"instance_id" ,nil ,false }, 27 {"fractional protocol" ,"protocol" ,3.5 ,false }, 28 {"unsupported protocol" ,"protocol" ,2 ,true }, 29 {"missing capabilities" ,"capabilities" ,nil ,false }, 30 {"unsupported capability revision" ,"capabilities" ,map [string ]any {},true }, 31 {"draining daemon" ,"state" ,"draining" ,false }, 32 {"invalid digest" ,"digest" ,7 ,false }, 33 } { 34t .Run (change .name ,func (t * testing.T ) { 35var resource map [string ]any 36_ = json .Unmarshal ([]byte (readyServer ),& resource ) 37resource [change .key ]= change .value 38server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 39if r .URL .Path != "/server" { 40t .Errorf ("unexpected route %s" ,r .URL .Path ) 41 } 42_ = json .NewEncoder (w ).Encode (resource ) 43 })) 44defer server .Close () 45conn ,err := Attach (t .Context (),ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"token" },nil ) 46if conn != nil || err == nil { 47t .Fatalf ("invalid server attached: %v %v" ,conn ,err ) 48 } 49if change .compatibility { 50if _ ,ok := errors.AsType [* CompatibilityError ](err );! ok { 51t .Fatalf ("compatibility cause lost: %v" ,err ) 52 } 53 } 54 }) 55 } 56} 57 58func TestOptionalCapabilitiesPreventRequestsWithoutRepeatedServerReads (t * testing.T ) { 59for _ ,feature := range []struct { 60name string 61run func (context.Context ,* Connection )error 62 }{ 63 {"context" ,func (ctx context.Context ,conn * Connection )error { 64_ ,err := GetContextSnapshot (ctx ,conn ,"s" ) 65return err 66 }}, 67 {"catalog" ,func (ctx context.Context ,conn * Connection )error { 68_ ,err := GetCapabilityCatalog (ctx ,conn ,"s" ) 69return err 70 }}, 71 {"workspace_browsing" ,func (ctx context.Context ,conn * Connection )error { 72_ ,err := ListFolders (ctx ,conn ,"/workspace" ) 73return err 74 }}, 75 {"host_probes" ,func (ctx context.Context ,conn * Connection )error {_ ,err := WarmHost (ctx ,conn ,"target" );return err }}, 76 {"provider_auth" ,func (ctx context.Context ,conn * Connection )error {_ ,err := SignInList (ctx ,conn );return err }}, 77 {"storage_report" ,func (ctx context.Context ,conn * Connection )error {_ ,err := GetStorageReport (ctx ,conn );return err }}, 78 } { 79t .Run (feature .name ,func (t * testing.T ) { 80var serverReads ,requests atomic.Int32 81server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 82if r .URL .Path == "/server" { 83serverReads .Add (1 ) 84_ ,_ = w .Write ([]byte (readyServer )) 85return 86 } 87requests .Add (1 ) 88w .WriteHeader (http .StatusNotFound ) 89 })) 90t .Cleanup (server .Close ) 91conn ,err := Attach (t .Context (),ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"token" },nil ) 92if err != nil { 93t .Fatal (err ) 94 } 95t .Cleanup (conn .HTTPClient ().CloseIdleConnections ) 96for range 2 { 97if _ ,ok := errors.AsType [* UpgradeRequiredError ](feature .run (t .Context (),conn ));! ok { 98t .Fatal ("the missing advertised optional capability was not refused" ) 99 } 100 } 101ctx ,cancel := context .WithCancel (t .Context ()) 102cancel () 103if err := feature .run (ctx ,conn );! errors .Is (err ,context .Canceled ) { 104t .Fatalf ("caller cancellation was replaced by capability refusal: %v" ,err ) 105 } 106if serverReads .Load ()!= 1 || requests .Load ()!= 0 { 107t .Fatalf ("capability checks issued HTTP requests: server=%d operation=%d" ,serverReads .Load (),requests .Load ()) 108 } 109 }) 110 } 111} 112 113func TestOptionalRecoveryUsesReplacementEndpointCapabilities (t * testing.T ) { 114var replacementReads ,replacementRequests atomic.Int32 115replacement := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 116if r .URL .Path == "/server" { 117replacementReads .Add (1 ) 118_ ,_ = w .Write ([]byte (readyServer )) 119return 120 } 121replacementRequests .Add (1 ) 122w .WriteHeader (http .StatusNotFound ) 123 })) 124t .Cleanup (replacement .Close ) 125var originalReads ,originalRequests atomic.Int32 126original := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 127if r .URL .Path == "/server" { 128originalReads .Add (1 ) 129var resource map [string ]any 130_ = json .Unmarshal ([]byte (readyServer ),& resource ) 131resource ["capabilities" ].(map [string ]any )["catalog" ]= 1 132_ = json .NewEncoder (w ).Encode (resource ) 133return 134 } 135originalRequests .Add (1 ) 136w .Header ().Set ("WWW-Authenticate" ,"Bearer" ) 137w .WriteHeader (http .StatusUnauthorized ) 138_ ,_ = w .Write ([]byte (`{"type":"about:blank","title":"Unauthorized","status":401,"code":"authentication_required","detail":"expired bearer"}` )) 139 })) 140t .Cleanup (original .Close ) 141next := ConnectionSnapshot {Port :replacement .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"replacement" } 142conn ,err := Attach (t .Context (),ConnectionSnapshot {Port :original .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"original" },func (context.Context ) (ConnectionSnapshot ,error ) {return next ,nil }) 143if err != nil { 144t .Fatal (err ) 145 } 146t .Cleanup (conn .HTTPClient ().CloseIdleConnections ) 147_ ,err = GetCapabilityCatalog (t .Context (),conn ,"s" ) 148if _ ,ok := errors.AsType [* UpgradeRequiredError ](err );! ok { 149t .Fatalf ("replacement capability refusal was lost: %v" ,err ) 150 } 151endpoint := conn .Snapshot () 152if endpoint .Port != next .Port || endpoint .Token != next .Token || originalReads .Load ()!= 1 || originalRequests .Load ()!= 1 || replacementReads .Load ()!= 1 || replacementRequests .Load ()!= 0 { 153t .Fatalf ("recovery reused old endpoint capability facts: old=%d/%d new=%d/%d" ,originalReads .Load (),originalRequests .Load (),replacementReads .Load (),replacementRequests .Load ()) 154 } 155} 156 157func TestProbeServerPreservesAuthenticationAndCancellationWithoutRediscovery (t * testing.T ) { 158for _ ,cancelProbe := range []bool {false ,true } { 159t .Run (fmt .Sprint (cancelProbe ),func (t * testing.T ) { 160entered := make (chan struct {}) 161server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 162close (entered ) 163if cancelProbe { 164<- r .Context ().Done () 165return 166 } 167w .WriteHeader (http .StatusUnauthorized ) 168_ ,_ = fmt .Fprint (w ,`{"type":"about:blank","title":"Unauthorized","status":401,"code":"authentication_required","detail":"invalid bearer"}` ) 169 })) 170defer server .Close () 171var rediscoveries atomic.Int32 172conn := NewConnection (ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"old" },func (context.Context ) (ConnectionSnapshot ,error ) { 173rediscoveries .Add (1 ) 174return ConnectionSnapshot {},errors .New ("must not rediscover" ) 175 }) 176ctx ,cancel := context .WithCancel (t .Context ()) 177defer cancel () 178finished := make (chan error ,1 ) 179go func () {_ ,err := ProbeServer (ctx ,conn );finished <- err }() 180select { 181case <- entered : 182case <- time .After (time .Second ): 183t .Fatal ("server request never started" ) 184 } 185if cancelProbe { 186cancel () 187 } 188select { 189case err := <- finished : 190if cancelProbe { 191if ! errors .Is (err ,context .Canceled ) { 192t .Fatalf ("cancellation lost: %v" ,err ) 193 } 194 }else { 195failure ,ok := errors.AsType [* APIError ](err ) 196if ! ok || failure .StatusCode != http .StatusUnauthorized || failure .Code != "authentication_required" { 197t .Fatalf ("authentication lost: %v" ,err ) 198 } 199 } 200case <- time .After (time .Second ): 201t .Fatal ("server request ignored cancellation" ) 202 } 203if rediscoveries .Load ()!= 0 { 204t .Fatal ("server probe attempted rediscovery" ) 205 } 206 }) 207 } 208} 209 210func TestRefreshGateCancellationAndSuccessfulAttachment (t * testing.T ) { 211server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 212if r .URL .Path == "/server" { 213_ ,_ = fmt .Fprint (w ,readyServer ) 214return 215 } 216if r .Header .Get ("Authorization" )!= "Bearer new" { 217t .Errorf ("request used stale credential: %s" ,r .Header .Get ("Authorization" )) 218 } 219_ ,_ = fmt .Fprint (w ,`{"ok":true}` ) 220 })) 221defer server .Close () 222entered ,release := make (chan struct {}),make (chan struct {}) 223var calls atomic.Int32 224candidate := ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"new" ,Version :99 ,Build :"unverified" } 225conn := NewConnection (ConnectionSnapshot {Port :candidate .Port ,Token :"old" },func (ctx context.Context ) (ConnectionSnapshot ,error ) { 226calls .Add (1 ) 227close (entered ) 228select { 229case <- release : 230return candidate ,nil 231case <- ctx .Done (): 232return ConnectionSnapshot {},ctx .Err () 233 } 234 }) 235defer conn .HTTPClient ().CloseIdleConnections () 236finished := make (chan error ,1 ) 237go func () {finished <- conn .Refresh (t .Context ()) }() 238<- entered 239waiting ,cancel := context .WithCancel (t .Context ()) 240cancel () 241if err := conn .Refresh (waiting );! errors .Is (err ,context .Canceled ) { 242t .Fatalf ("refresh wait cancellation lost: %v" ,err ) 243 } 244close (release ) 245if err := <- finished ;err != nil { 246t .Fatal (err ) 247 } 248if calls .Load ()!= 1 { 249t .Fatalf ("canceled waiter rediscovered: %d" ,calls .Load ()) 250 } 251endpoint := conn .Snapshot () 252if endpoint .Version != ProtocolVersion || endpoint .Build != "verified-build" || endpoint .Digest != "verified-digest" { 253t .Fatalf ("attachment trusted unverified discovery: %+v" ,endpoint ) 254 } 255if _ ,err := requestBytes (t .Context (),conn ,operation {Name :"read" ,Method :http .MethodGet ,Path :"/read" ,Policy :readRecovery },responseLimits {bodyBytes :50 * 1024 * 1024 ,errorBytes :64 * 1024 });err != nil { 256t .Fatal (err ) 257 } 258} 259 260func TestOperationSurfacesRefreshFailureWithoutReplaying (t * testing.T ) { 261for _ ,malformed := range []bool {false ,true } { 262t .Run (fmt .Sprint (malformed ),func (t * testing.T ) { 263var operations ,rediscoveries atomic.Int32 264server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 265if r .URL .Path == "/server" { 266_ ,_ = fmt .Fprint (w ,`{"instance_id":"bad","protocol":3,"capabilities":null}` ) 267return 268 } 269operations .Add (1 ) 270w .WriteHeader (http .StatusUnauthorized ) 271_ ,_ = fmt .Fprint (w ,`{"type":"about:blank","title":"Unauthorized","status":401,"code":"authentication_required","detail":"invalid bearer"}` ) 272 })) 273defer server .Close () 274cause := errors .New ("discovery inaccessible" ) 275conn := NewConnection (ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"old" },func (context.Context ) (ConnectionSnapshot ,error ) { 276rediscoveries .Add (1 ) 277if ! malformed { 278return ConnectionSnapshot {},cause 279 } 280return ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"new" },nil 281 }) 282_ ,err := requestBytes (t .Context (),conn ,operation {Name :"change" ,Method :http .MethodPost ,Path :"/sessions/s/inputs/i" ,Policy :authRecovery },responseLimits {bodyBytes :50 * 1024 * 1024 ,errorBytes :64 * 1024 }) 283if malformed { 284if _ ,ok := errors.AsType [* ProtocolError ](err );! ok { 285t .Fatalf ("server failure hidden: %v" ,err ) 286 } 287 }else if ! errors .Is (err ,cause ) { 288t .Fatalf ("discovery failure hidden: %v" ,err ) 289 } 290if operations .Load ()!= 1 || rediscoveries .Load ()!= 1 || conn .Snapshot ().Token != "old" { 291t .Fatalf ("failed refresh changed endpoint or replayed: operations=%d rediscoveries=%d token=%s" ,operations .Load (),rediscoveries .Load (),conn .Snapshot ().Token ) 292 } 293 }) 294 } 295} 296 297func TestBuildMismatchStaysQuietOnlyForProvenSameness (t * testing.T ) { 298for _ ,tc := range []struct { 299running ,selected BuildIdentity 300want bool 301 }{ 302 {running :BuildIdentity {Digest :"a" },selected :BuildIdentity {Digest :"a" }}, 303 {running :BuildIdentity {Build :"same" },selected :BuildIdentity {Build :"same" }}, 304 {running :BuildIdentity {Digest :"a" },selected :BuildIdentity {Digest :"b" },want :true }, 305 {running :BuildIdentity {Build :"x" },selected :BuildIdentity {Build :"y" },want :true }, 306// Digests outrank labels: identical content is one build even when 307// the labels disagree, and different content is a mismatch even when 308// the labels agree. 309 {running :BuildIdentity {Build :"old" ,Digest :"a" },selected :BuildIdentity {Build :"new" ,Digest :"a" }}, 310 {running :BuildIdentity {Build :"same" ,Digest :"a" },selected :BuildIdentity {Build :"same" ,Digest :"b" },want :true }, 311// Unprovable: one side of each identity is unknown. 312 {running :BuildIdentity {Digest :"a" },selected :BuildIdentity {Build :"y" },want :true }, 313 {running :BuildIdentity {Build :"x" },selected :BuildIdentity {Digest :"b" },want :true }, 314 {want :true }, 315 } { 316if got := BuildMismatch (tc .running ,tc .selected );got != tc .want { 317t .Fatalf ("BuildMismatch(%+v, %+v) = %v, want %v" ,tc .running ,tc .selected ,got ,tc .want ) 318 } 319 } 320}