okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
ed54422
main
1// Count requests across preparation and TUI initialization, including notice 2// acknowledgements. Daemon E2E cannot inspect the detached UI's startup commands. 3package terminal 4 5import ( 6"context" 7"encoding/json" 8"io" 9"net" 10"net/http" 11"net/http/httptest" 12"strings" 13"testing" 14 15"albedo/cli/internal/app" 16"albedo/cli/internal/daemon" 17"albedo/cli/internal/testwire" 18"albedo/cli/internal/tui" 19 tea"charm.land/bubbletea/v2" 20) 21 22func runInitialCommands (model tea.Model ,command tea.Cmd ) { 23if command == nil { 24return 25 } 26msg := command () 27if batch ,ok := msg .(tea.BatchMsg );ok { 28for _ ,child := range batch { 29runInitialCommands (model ,child ) 30 } 31return 32 } 33// Follow-up commands include recurring UI timers and stream subscriptions. 34_ ,_ = model .Update (msg ) 35} 36 37func TestPreparedStartupReusesReadsAndRefreshesNoticeValidator (t * testing.T ) { 38for _ ,scenario := range []struct { 39name string 40selected ,fresh ,login ,notice bool 41 }{ 42 {name :"picker" }, 43 {name :"acknowledged notice" ,notice :true }, 44 {name :"resume" ,selected :true }, 45 {name :"fresh" ,fresh :true }, 46 {name :"login" ,login :true }, 47 } { 48t .Run (scenario .name ,func (t * testing.T ) { 49settingsReads ,sessionLists := 0 ,0 50acknowledged := false 51server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 52switch { 53case r .URL .Path == "/server" : 54var resource map [string ]any 55_ = json .Unmarshal ([]byte (testwire .Server ),& resource ) 56if scenario .notice { 57resource ["notices" ]= []any {map [string ]any {"id" :"migration" ,"kind" :"migration" ,"message" :"Updated settings" }} 58 } 59_ = json .NewEncoder (w ).Encode (resource ) 60case r .URL .Path == "/sessions" : 61sessionLists ++ 62items := []any {} 63if ! scenario .login { 64items = append (items ,testwire .Session ("existing" ,testwire .GenerationA ,0 )) 65 } 66_ = json .NewEncoder (w ).Encode (map [string ]any {"items" :items ,"next" :nil }) 67case strings .HasPrefix (r .URL .Path ,"/sessions/" ): 68id := strings .TrimPrefix (r .URL .Path ,"/sessions/" ) 69if r .Method == http .MethodPut { 70w .WriteHeader (http .StatusCreated ) 71 } 72_ = json .NewEncoder (w ).Encode (testwire .Session (id ,testwire .GenerationA ,0 )) 73case r .URL .Path == "/settings" : 74settings := testwire .Settings () 75if ! scenario .login { 76settings ["providers" ].(map [string ]any )["default_profile" ]= "work" 77 } 78ui := settings ["ui" ].(map [string ]any ) 79if r .Method == http .MethodPatch { 80if r .Header .Get ("If-Match" )!= `"ui-a"` { 81t .Errorf ("notice acknowledgement used stale validator: %s" ,r .Header .Get ("If-Match" )) 82 } 83acknowledged = true 84ui ["dismissed_notices" ]= []any {"migration" } 85_ = json .NewEncoder (w ).Encode (testwire .SettingsChange ("ui" ,ui )) 86return 87 } 88settingsReads ++ 89if acknowledged { 90ui ["dismissed_notices" ]= []any {"migration" } 91settings ["group_resources" ].(map [string ]any )["ui" ].(map [string ]any )["etag" ]= `"ui-b"` 92 } 93_ = json .NewEncoder (w ).Encode (settings ) 94case r .URL .Path == "/auth" : 95_ ,_ = io .WriteString (w ,`{"providers":[],"accounts":[]}` ) 96default : 97t .Errorf ("unexpected startup request: %s %s" ,r .Method ,r .URL ) 98http .NotFound (w ,r ) 99 } 100 })) 101t .Cleanup (server .Close ) 102conn ,err := daemon .Attach (t .Context (), daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"token" },nil ) 103if err != nil { 104t .Fatal (err ) 105 } 106t .Cleanup (conn .HTTPClient ().CloseIdleConnections ) 107workflow := app.Service {Connect :func (_ context.Context ) (* daemon.Connection ,error ) {return conn ,nil }} 108options := app.OpenOptions {Terminal :true ,Fresh :scenario .fresh } 109if scenario .selected { 110options .SessionID = "existing" 111 } 112prepared ,err := workflow .PrepareOpen (t .Context (),options ) 113if err != nil { 114t .Fatal (err ) 115 } 116service := Service {In :strings .NewReader ("\n" ),Out :io .Discard } 117settings ,err := service .announceMigration (t .Context (),conn ,prepared .Settings ) 118if err != nil { 119t .Fatal (err ) 120 } 121model := tui .NewAppModel (conn , tui.Bootstrap {Sessions :prepared .Sessions ,Settings :settings },prepared .Selected ,prepared .Workspace ,prepared .LoginRequired ,nil ) 122t .Cleanup (func () {_ = model .Close () }) 123if scenario .selected || scenario .fresh { 124if model .ActiveSession == nil || ! model .Chat .Flags .Thinking || model .Chat .Flags .Tools { 125t .Fatal ("chat did not start from prepared preferences" ) 126 } 127found := false 128for _ ,session := range model .Sessions { 129found = found || session .ID == model .ActiveSession .ID && session .ETag != "" 130 } 131if ! found { 132t .Fatal ("selected or created session missing from bootstrap" ) 133 } 134 }else { 135runInitialCommands (model ,model .Init ()) 136if model .SessionPicker .Loading || len (model .Sessions )!= len (prepared .Sessions ) { 137t .Fatal ("prepared list was discarded, including its known empty state" ) 138 } 139 } 140wantSettings := 1 141if scenario .notice { 142wantSettings ++ 143if model .UI .ETag != `"ui-b"` || len (model .UI .DismissedNotices )!= 1 { 144t .Fatalf ("TUI seeded before notice acknowledgement: %+v" ,model .UI ) 145 } 146 } 147if sessionLists != 1 || settingsReads != wantSettings { 148t .Fatalf ("startup read sessions %d times and settings %d times, want 1 and %d" ,sessionLists ,settingsReads ,wantSettings ) 149 } 150 }) 151 } 152}