okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
82b9457
main
1//go:build unix 2 3// Acknowledgement loss must not duplicate effects in the real daemon. The 4// proxy waits for admission before cutting the client response, so these 5// scenarios cannot accidentally test a failure that preceded execution. 6package e2e 7 8import ( 9"context" 10"encoding/json" 11"errors" 12"net/http" 13"slices" 14"strings" 15"sync" 16"sync/atomic" 17 18 tea"charm.land/bubbletea/v2" 19"testing" 20"time" 21 22"albedo/cli/internal/app" 23"albedo/cli/internal/daemon" 24"albedo/cli/internal/localdaemon" 25"albedo/cli/internal/tui" 26) 27 28func TestLostCreationAcknowledgementCreatesOneSession (t * testing.T ) { 29providerRoute (t ,echoReply ) 30// Default-changing tests finish before parallel bodies. These creations 31// send no turns and do not assert which provider the daemon selects. 32t .Parallel () 33for _ ,fault := range []acknowledgementFault {dropAcknowledgement ,truncateAcknowledgement ,rejectAcknowledgement ,emptyAcknowledgement ,malformedAcknowledgement ,missingAcknowledgement } { 34t .Run (string (fault ),func (t * testing.T ) { 35workspace := t .TempDir () 36proxy := cutAcknowledgement (t ,"/sessions" ,fault ) 37service := app.Service {Connect :func (context.Context ) (* daemon.Connection ,error ) {return proxy .connection ,nil }} 38opened ,err := service .PrepareOpen (context .Background (), app.OpenOptions {Workspace :workspace ,Fresh :true }) 39if err != nil { 40t .Fatal (err ) 41 } 42accepted := proxy .acceptedResponse (t ) 43var created struct { 44ID string `json:"id"` 45 } 46if decodeErr := json .Unmarshal (accepted ,& created );decodeErr != nil || created .ID == "" { 47t .Fatalf ("accepted session: %s, %v" ,accepted ,decodeErr ) 48 } 49if opened .Selected == nil || opened .Selected .ID != created .ID { 50t .Fatal ("lost creation receipt was not recovered" ) 51 } 52sessions ,err := daemon .ListSessions (context .Background (),conn (t )) 53if err != nil { 54t .Fatal (err ) 55 } 56count := 0 57for _ ,session := range sessions { 58if session .Workspace == workspace { 59count ++ 60 } 61 } 62if count != 1 { 63t .Fatalf ("workspace contains %d sessions, want one" ,count ) 64 } 65 }) 66 } 67} 68 69func TestRejectedCreationReceiptRecoversWithoutAllocatingSession (t * testing.T ) { 70t .Parallel () 71workspace := t .TempDir () 72handle ,err := daemon .NewCreation (daemon.CreateSessionRequest {Workspace :workspace ,Provider :"missing-creation-profile" }) 73if err != nil { 74t .Fatal (err ) 75 } 76_ ,err = daemon .CreateSessionOperation (t .Context (),conn (t ),handle ) 77initial ,ok := errors.AsType [* daemon.APIError ](err ) 78if ! ok || initial .StatusCode != http .StatusBadRequest { 79t .Fatalf ("unknown provider did not reject creation: %v" ,err ) 80 } 81// The creation resource retains its rejection without allocating a session. 82created ,err := daemon .ResolveCreation (t .Context (),conn (t ),handle ) 83recovered ,ok := errors.AsType [* daemon.APIError ](err ) 84if ! ok || recovered .StatusCode != initial .StatusCode || created .ID != "" { 85t .Fatalf ("resolve rejected creation: %+v, %v" ,created ,err ) 86 } 87sessions ,err := daemon .ListSessions (t .Context (),conn (t )) 88if err != nil { 89t .Fatal (err ) 90 } 91for _ ,session := range sessions { 92if session .Workspace == workspace { 93t .Fatal ("rejected creation allocated a session" ) 94 } 95 } 96} 97 98func TestInvalidTUICreationAcknowledgementRecoversCreatedSession (t * testing.T ) { 99profile := providerRoute (t ,echoReply ) 100// Default-changing tests finish before parallel bodies. These creations 101// send no turns and do not assert which provider the daemon selects. 102t .Parallel () 103for _ ,fault := range []acknowledgementFault {dropAcknowledgement ,emptyAcknowledgement ,malformedAcknowledgement ,missingAcknowledgement } { 104t .Run (string (fault ),func (t * testing.T ) { 105initial := daemonSession (t ,newFaultSession (t ,profile )) 106workspace := t .TempDir () 107proxy := cutAcknowledgement (t ,"/sessions" ,fault ) 108driver := driveTUIWithConnection (t ,& initial ,proxy .connection ) 109defer driver .App .Chat .Close () 110result := driver .Send (tui.FolderNewSessionMsg {Workspace :workspace }) 111driver .Update (result ) 112accepted := proxy .acceptedResponse (t ) 113var created struct { 114ID string `json:"id"` 115 } 116if decodeErr := json .Unmarshal (accepted ,& created );decodeErr != nil || created .ID == "" { 117t .Fatalf ("accepted session: %s, %v" ,accepted ,decodeErr ) 118 } 119if driver .App .ActiveSession == nil || driver .App .ActiveSession .ID != created .ID || driver .App .Chat .Notices .HasError () { 120t .Fatalf ("lost creation response did not recover its created session: %+v" ,driver .App .Chat .Notices ) 121 } 122count := 0 123for _ ,session := range daemonSessions (t ) { 124if session .Workspace == workspace { 125count ++ 126 } 127 } 128if count != 1 { 129t .Fatalf ("TUI creation admitted %d sessions, want one" ,count ) 130 } 131 }) 132 } 133} 134 135func TestInvalidSubmissionAcknowledgementSendsOneTurn (t * testing.T ) { 136for _ ,fault := range []acknowledgementFault {dropAcknowledgement ,emptyAcknowledgement ,malformedAcknowledgement ,missingAcknowledgement ,negativeAcknowledgement ,wrongStatusAcknowledgement } { 137t .Run (string (fault ),func (t * testing.T ) { 138profile := "invalid-submit-" + string (fault ) 139if err := suite .provider .addProfile (profile ,echoReply );err != nil { 140t .Fatal (err ) 141 } 142id := newFaultSession (t ,profile ) 143proxy := cutAcknowledgement (t ,"/sessions/" + id + "/inputs/" ,fault ) 144client := daemon .NewChatClient (proxy .connection ,id ) 145result ,err := client .Send (context .Background (),"exactly one uncertain turn" ,nil ) 146if err != nil || result == nil || result .OperationID == "" { 147t .Fatalf ("recover submission: %+v %v" ,result ,err ) 148 } 149proxy .acceptedResponse (t ) 150waitIdle (t ,id ,profile ,1 ) 151if count := len (suite .provider .requests (profile ));count != 1 { 152t .Fatalf ("provider received %d turns, want one" ,count ) 153 } 154history ,err := daemon .NewChatClient (conn (t ),id ).History (context .Background (),0 ,120 ) 155if err != nil { 156t .Fatal (err ) 157 } 158count := 0 159for _ ,event := range history .Events { 160if event .Type == daemon .EventUser { 161count ++ 162 } 163 } 164if count != 1 { 165t .Fatalf ("transcript contains %d user submissions, want one" ,count ) 166 } 167 }) 168 } 169} 170 171func TestInvalidCommandAcknowledgementAddsOneWorkItem (t * testing.T ) { 172profile := providerRoute (t ,echoReply ) 173t .Parallel () 174for _ ,fault := range []acknowledgementFault {truncateAcknowledgement ,emptyAcknowledgement ,malformedAcknowledgement ,missingAcknowledgement ,negativeAcknowledgement ,wrongStatusAcknowledgement } { 175t .Run (string (fault ),func (t * testing.T ) { 176id := newFaultSession (t ,profile ) 177path := "/extensions/work/items" 178proxy := cutAcknowledgement (t ,path ,fault ) 179page ,err := daemon .LoadPage (t .Context (),conn (t ),id ,"/work" ) 180if err != nil { 181t .Fatal (err ) 182 } 183var action * daemon.PageAction 184for index := range page .Actions { 185candidate := & page .Actions [index ] 186if candidate .Operation .Method == http .MethodPost && candidate .Operation .PathTemplate == path { 187action = candidate 188break 189 } 190 } 191if action == nil { 192t .Fatal ("work creation action missing" ) 193 } 194_ ,err = daemon .ExecutePageAction (t .Context (),proxy .connection , daemon.PageActionRequest {Action :* action ,Session :page .Session ,Form :map [string ]json.RawMessage {"title" :json .RawMessage (`"one uncertain work item"` ),"notes" :json .RawMessage (`""` ),"parent_id" :json .RawMessage (`null` )}}) 195proxy .assertSingleEffect (t ,err ) 196listed ,err := daemon .LoadPage (context .Background (),conn (t ),id ,"/work" ) 197if err != nil { 198t .Fatal (err ) 199 } 200if len (listed .Rows )!= 1 || listed .Rows [0 ].Text != "one uncertain work item" { 201t .Fatalf ("ledger after lost acknowledgement: %+v" ,listed ) 202 } 203 }) 204 } 205} 206 207func TestCancellationAfterAdmissionPreservesUncertainCause (t * testing.T ) { 208profile := providerRoute (t ,echoReply ) 209t .Parallel () 210proxy := cutAcknowledgement (t ,"/sessions" ,cancelAcknowledgement ) 211ctx ,cancel := context .WithCancel (context .Background ()) 212defer cancel () 213result := make (chan error ,1 ) 214workspace := t .TempDir () 215go func () { 216_ ,err := daemon .CreateSession (ctx ,proxy .connection , daemon.CreateSessionRequest {Workspace :workspace ,Provider :profile }) 217result <- err 218 }() 219select { 220case <- proxy .admitted : 221cancel () 222case <- time .After (20 * time .Second ): 223t .Fatal ("mutation was not admitted" ) 224 } 225select { 226case err := <- result : 227proxy .assertSingleEffect (t ,err ) 228if ! errors .Is (err ,context .Canceled ) { 229t .Fatalf ("uncertain error lost cancellation cause: %v" ,err ) 230 } 231uncertain ,ok := errors.AsType [* daemon.UncertainOutcomeError ](err ) 232if ! ok || uncertain .Handle == nil { 233t .Fatal ("cancellation lost the operation handle" ) 234 } 235original ,resolveErr := daemon .ResolveCreation (t .Context (),proxy .connection ,uncertain .Handle ) 236if resolveErr != nil || original .ID == "" { 237t .Fatalf ("resolve original creation: %+v %v" ,original ,resolveErr ) 238 } 239proxy .acceptedResponse (t ) 240case <- time .After (5 * time .Second ): 241t .Fatal ("canceled operation did not return" ) 242 } 243} 244 245func TestLostSignInAcknowledgementStartsOneLocalFlow (t * testing.T ) { 246proxy := cutAcknowledgement (t ,"/auth/logins/" ,dropAcknowledgement ) 247recovered ,err := daemon .StartProviderLogin (context .Background (),proxy .connection ,"antigravity" ,"" ,nil ) 248if err != nil { 249t .Fatal (err ) 250 } 251accepted := proxy .acceptedResponse (t ) 252var started daemon.StartedSignIn 253if decodeErr := json .Unmarshal (accepted ,& started );decodeErr != nil || started .ID == "" { 254t .Fatalf ("accepted sign-in: %s, %v" ,accepted ,decodeErr ) 255 } 256if recovered .ID != started .ID { 257t .Fatal ("sign-in recovery changed the chosen ID" ) 258 } 259t .Cleanup (func () { 260if cancelErr := daemon .CancelSignIn (context .Background (),conn (t ),started .ID );cancelErr != nil { 261t .Errorf ("cancel sign-in: %v" ,cancelErr ) 262 } 263 }) 264status ,err := daemon .PollSignIn (context .Background (),conn (t ),started .ID ) 265if err != nil || status .State != "waiting" { 266t .Fatalf ("the one admitted local flow: %+v, %v" ,status ,err ) 267 } 268// Starting only allocates a loopback listener and builds an authorization 269// URL. No callback or pasted code is supplied, so no exchange reaches a provider. 270} 271 272func TestCoreAuthenticationRefusalRecoversBeforeCreation (t * testing.T ) { 273profile := providerRoute (t ,echoReply ) 274t .Parallel () 275snapshot := conn (t ).Snapshot () 276snapshot .Token = "expired-client-token" 277stale := daemon .NewConnection (snapshot ,func (ctx context.Context ) (daemon.ConnectionSnapshot ,error ) { 278return localdaemon .Rediscover (ctx ,suite .home ) 279 }) 280t .Cleanup (stale .HTTPClient ().CloseIdleConnections ) 281workspace := t .TempDir () 282created ,err := daemon .CreateSession (context .Background (),stale , daemon.CreateSessionRequest {Workspace :workspace ,Provider :profile }) 283if err != nil || created .ID == "" { 284t .Fatalf ("creation after pre-admission refusal: %+v, %v" ,created ,err ) 285 } 286if stale .Snapshot ().Token != conn (t ).Snapshot ().Token { 287t .Fatal ("recovery did not update credentials" ) 288 } 289sessions ,err := daemon .ListSessions (context .Background (),conn (t )) 290if err != nil { 291t .Fatal (err ) 292 } 293count := 0 294for _ ,session := range sessions { 295if session .Workspace == workspace { 296count ++ 297 } 298 } 299if count != 1 { 300t .Fatalf ("authentication recovery created %d sessions, want one" ,count ) 301 } 302} 303 304func newFaultSession (t * testing.T ,profile string )string { 305t .Helper () 306created ,err := daemon .CreateSession (t .Context (),conn (t ), daemon.CreateSessionRequest {Workspace :t .TempDir (),Provider :profile }) 307if err != nil { 308t .Fatal (err ) 309 } 310return created .ID 311} 312 313func TestWebhookCreationMissingGeneratedSecretStopsAfterOneEffect (t * testing.T ) { 314profile := providerRoute (t ,echoReply ) 315id := newFaultSession (t ,profile ) 316enableExtension (t ,conn (t ),id ,"webhooks" ) 317path := "/extensions/webhooks/hooks" 318proxy := mutateAcknowledgement (t ,path ,"" ,nil ,func (body []byte ) ([]byte ,error ) { 319var envelope map [string ]json.RawMessage 320if err := json .Unmarshal (body ,& envelope );err != nil { 321return nil ,err 322 } 323if _ ,present := envelope ["secret" ];! present { 324return nil ,errors .New ("daemon did not generate a secret" ) 325 } 326delete (envelope ,"secret" ) 327return json .Marshal (envelope ) 328 }) 329session := daemonSession (t ,id ) 330driver := driveTUIWithConnection (t ,& session ,proxy .connection ) 331defer driver .App .Chat .Close () 332driver .Dispatch (tui.ChatOpenWebhooksPageMsg {}) 333if ! driver .App .WebhooksPage .Loaded || driver .App .WebhooksPage .Error != "" { 334t .Fatalf ("open creation screen: %s" ,driver .App .WebhooksPage .Error ) 335 } 336driver .Dispatch (tea.KeyPressMsg {Code :'o' ,Mod :tea .ModCtrl }) 337driver .Type ("missing-secret" ) 338// Creation installs the signature atomically even when its secret acknowledgment is lost. 339driver .App .WebhooksPage .Form .Inputs ["header" ].SetValue ("x-custom-signature" ) 340proxy .mu .Lock () 341before := len (proxy .requests ) 342proxy .mu .Unlock () 343driver .Dispatch (tea.KeyPressMsg {Code :'s' ,Mod :tea .ModCtrl }) 344accepted := proxy .acceptedResponse (t ) 345page := driver .App .WebhooksPage 346if page .Saving || page .Loading || page .Form == nil || page .Error == "" || page .Notice != "" || page .Reveal != nil { 347t .Fatalf ("uncertain creation state: %+v" ,page ) 348 } 349proxy .mu .Lock () 350requests := slices .Clone (proxy .requests [before :]) 351proxy .mu .Unlock () 352if ! slices .Equal (requests , []string {"POST " + path }) { 353t .Fatalf ("creation replayed, configured a signature, or refreshed: %v" ,requests ) 354 } 355var original struct { 356Resource struct { 357ETag string `json:"etag"` 358Value struct { 359ID string `json:"id"` 360 }`json:"value"` 361 }`json:"resource"` 362 } 363if err := json .Unmarshal (accepted ,& original );err != nil { 364t .Fatal (err ) 365 } 366t .Cleanup (func () { 367if _ ,err := daemon .DeleteWebhook (context .Background (),conn (t ),original .Resource .Value .ID ,original .Resource .ETag );err != nil { 368t .Error (err ) 369 } 370 }) 371listed ,err := daemon .ListWebhooks (t .Context (),conn (t ),id ) 372if err != nil { 373t .Fatal (err ) 374 } 375count := 0 376for _ ,entry := range listed { 377if entry .Hook .Session == id && entry .Hook .Name == "missing-secret" { 378count ++ 379if entry .Hook .Header != "x-custom-signature" { 380t .Fatal ("atomic signature configuration was lost" ) 381 } 382 } 383 } 384if count != 1 { 385t .Fatalf ("persisted %d hooks, want one" ,count ) 386 } 387} 388 389func TestUnknownReceiptReplaysOriginalCreationOnce (t * testing.T ) { 390providerRoute (t ,echoReply ) 391// Default-changing tests finish before parallel bodies. These creations 392// send no turns and do not assert which provider the daemon selects. 393t .Parallel () 394workspace := t .TempDir () 395proxy := cutAcknowledgement (t ,"/sessions" ,hideReceiptAcknowledgement ) 396handle ,err := daemon .NewCreation (daemon.CreateSessionRequest {Workspace :workspace }) 397if err != nil { 398t .Fatal (err ) 399 } 400created ,err := daemon .CreateSessionOperation (t .Context (),proxy .connection ,handle ) 401if err != nil { 402t .Fatal (err ) 403 } 404proxy .mu .Lock () 405responses := slices .Clone (proxy .responses ) 406proxy .mu .Unlock () 407if len (responses )!= 2 { 408t .Fatalf ("expected the original PUT and one conditional retry, got %d" ,len (responses )) 409 } 410requests := operationRequests (proxy ) 411if len (requests )!= 4 || requests [0 ]!= requests [2 ]|| requests [1 ]!= requests [3 ] { 412t .Fatalf ("retry changed the original resource: %v" ,requests ) 413 } 414count := 0 415for _ ,session := range daemonSessions (t ) { 416if session .Workspace == workspace { 417count ++ 418 } 419 } 420if count != 1 { 421t .Fatalf ("replay created %d sessions" ,count ) 422 } 423resolved ,err := daemon .ResolveCreation (t .Context (),proxy .connection ,handle ) 424if err != nil || resolved .ID != created .ID { 425t .Fatalf ("original creation: %+v %v" ,resolved ,err ) 426 } 427} 428 429func TestQueuedPromptDeadlineCancelsOnlyItsOperation (t * testing.T ) { 430entered ,release := make (chan struct {}),make (chan struct {}) 431released := false 432defer func () { 433if ! released { 434close (release ) 435 } 436 }() 437profile := providerRoute (t ,func (request map [string ]any )string { 438if lastUserText (request )== "active" { 439close (entered ) 440<- release 441 } 442return echoReply (request ) 443 }) 444t .Parallel () 445id := newSession (t ,t .TempDir ()) 446if _ ,err := daemon .NewChatClient (conn (t ),id ).Send (t .Context (),"active" ,nil );err != nil { 447t .Fatal (err ) 448 } 449select { 450case <- entered : 451case <- time .After (10 * time .Second ): 452t .Fatal ("active turn never started" ) 453 } 454connection ,attachErr := daemon .Attach (t .Context (),conn (t ).Snapshot (),nil ) 455if attachErr != nil { 456t .Fatal (attachErr ) 457 } 458counter := & submissionCounterTransport {next :connection .HTTPClient ().Transport ,accepted :make (chan struct {})} 459connection .HTTPClient ().Transport = counter 460t .Cleanup (connection .HTTPClient ().CloseIdleConnections ) 461service := app.Service {Connect :func (context.Context ) (* daemon.Connection ,error ) {return connection ,nil }} 462finished := make (chan error ,1 ) 463go func () { 464_ ,err := service .RunPrompt (t .Context (), app.PromptOptions {SessionID :id ,Prompt :"waiting" ,Timeout :2 * time .Second }) 465finished <- err 466 }() 467select { 468case <- counter .accepted : 469case err := <- finished : 470t .Fatalf ("prompt ended before durable acceptance: %v" ,err ) 471case <- time .After (5 * time .Second ): 472t .Fatal ("queued prompt was not admitted" ) 473 } 474err := <- finished 475uncertain ,ok := errors.AsType [* daemon.UncertainOutcomeError ](err ) 476if ! ok || uncertain .Handle == nil || ! errors .Is (err ,context .DeadlineExceeded ) { 477t .Fatalf ("deadline lost operation identity: %v" ,err ) 478 } 479receipt ,err := daemon .ResolveOperation (t .Context (),connection ,uncertain .Handle ) 480if err != nil || receipt .Delivery == nil || * receipt .Delivery != "cancelled" { 481t .Fatalf ("queued receipt after deadline: %+v %v" ,receipt ,err ) 482 } 483if counter .posts .Load ()!= 1 { 484t .Fatal ("query recovery submitted another intent" ) 485 } 486close (release ) 487released = true 488waitIdle (t ,id ,profile ,1 ) 489receipt ,err = daemon .ResolveOperation (t .Context (),connection ,uncertain .Handle ) 490if err != nil || receipt .Delivery == nil || * receipt .Delivery != "cancelled" { 491t .Fatalf ("cancelled receipt after unrelated turn: %+v %v" ,receipt ,err ) 492 } 493if counter .posts .Load ()!= 1 { 494t .Fatal ("query recovery submitted another intent" ) 495 } 496history ,err := daemon .NewChatClient (conn (t ),id ).History (t .Context (),0 ,120 ) 497if err != nil { 498t .Fatal (err ) 499 } 500matched := 0 501for _ ,event := range history .Events { 502if event .Type == daemon .EventUser && event .OperationID == uncertain .Handle .ID () { 503matched ++ 504 } 505 } 506if matched != 0 { 507t .Fatalf ("cancelled deadline operation delivered %d times" ,matched ) 508 } 509} 510 511// Count the real client's submissions while leaving its SSE stream unbuffered. 512type submissionCounterTransport struct { 513next http.RoundTripper 514posts atomic.Int32 515accepted chan struct {} 516acceptOnce sync.Once 517} 518 519func (transport * submissionCounterTransport )RoundTrip (request * http.Request ) (* http.Response ,error ) { 520if request .Method == http .MethodPut && strings .Contains (request .URL .Path ,"/inputs/" ) { 521transport .posts .Add (1 ) 522 } 523response ,err := transport .next .RoundTrip (request ) 524if err == nil && request .Method == http .MethodPut && strings .Contains (request .URL .Path ,"/inputs/" )&& response .StatusCode == http .StatusAccepted { 525transport .acceptOnce .Do (func () {close (transport .accepted ) }) 526 } 527return response ,err 528} 529func (transport * submissionCounterTransport )CloseIdleConnections () { 530if closer ,ok := transport .next .(interface {CloseIdleConnections () });ok { 531closer .CloseIdleConnections () 532 } 533} 534 535func TestTUICreationRecoveryRetainsSelectedSession (t * testing.T ) { 536profile := providerRoute (t ,echoReply ) 537// Default-changing tests finish before parallel bodies. These creations 538// send no turns and do not assert which provider the daemon selects. 539t .Parallel () 540initial := daemonSession (t ,newFaultSession (t ,profile )) 541other := daemonSession (t ,newFaultSession (t ,profile )) 542workspace := t .TempDir () 543proxy := cutAcknowledgement (t ,"/sessions" ,unresolvedAcknowledgement ) 544driver := driveTUIWithConnection (t ,& initial ,proxy .connection ) 545t .Cleanup (func () {driver .App .Chat .Close () }) 546uncertain := driver .Send (tui.FolderNewSessionMsg {Workspace :workspace }) 547driver .Update (tui.FolderOpenSessionMsg {Session :other }) 548recovery := driver .Update (uncertain ) 549if recovery == nil { 550t .Fatal ("navigation discarded uncertain creation recovery" ) 551 } 552proxy .allowReceipt .Store (true ) 553for _ ,recoveryRequest := range driver .results (recovery ) { 554driver .Update (driver .Send (recoveryRequest )) 555 } 556if driver .App .ActiveSession == nil || driver .App .ActiveSession .ID != other .ID { 557t .Fatal ("recovering older creation replaced the user's selected session" ) 558 } 559count := 0 560for _ ,session := range driver .App .Sessions { 561if session .Workspace == workspace { 562count ++ 563 } 564 } 565if count != 1 { 566t .Fatalf ("navigation recovered %d created sessions" ,count ) 567 } 568proxy .mu .Lock () 569responses := slices .Clone (proxy .responses ) 570proxy .mu .Unlock () 571var first struct { 572ID string `json:"id"` 573 } 574var replay struct { 575Decision struct { 576SessionID string `json:"session_id"` 577Admission string `json:"admission"` 578HTTPStatus int `json:"http_status"` 579 }`json:"decision"` 580 } 581if len (responses )!= 2 || json .Unmarshal (responses [0 ],& first )!= nil || first .ID == "" || json .Unmarshal (responses [1 ],& replay )!= nil || replay .Decision .SessionID != first .ID || replay .Decision .Admission != "accepted" || replay .Decision .HTTPStatus != 201 { 582t .Fatalf ("creation retry lost its original accepted decision: %d responses, identity=%q replay=%+v" ,len (responses ),first .ID ,replay .Decision ) 583 } 584if recovered ,err := daemon .GetSession (t .Context (),conn (t ),first .ID );err != nil || recovered .Workspace != workspace { 585t .Fatalf ("recovery lost created session: %v" ,err ) 586 } 587count = 0 588for _ ,session := range daemonSessions (t ) { 589if session .Workspace == workspace { 590count ++ 591 } 592 } 593if count != 1 { 594t .Fatalf ("creation uncertainty produced %d durable sessions" ,count ) 595 } 596} 597 598// Receipt fault tests also navigate sessions, whose event streams must remain live.