okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
ed54422
main
1// Delayed controlled responses verify that commands retain captured UI inputs. 2package tui 3 4import ( 5"albedo/cli/internal/config" 6"albedo/cli/internal/daemon" 7"albedo/cli/internal/testwire" 8 tea"charm.land/bubbletea/v2" 9"encoding/json" 10"net" 11"net/http" 12"net/http/httptest" 13"reflect" 14"sync" 15"testing" 16"time" 17) 18 19func commandTestConnection (t * testing.T ,handler http.HandlerFunc )* daemon.Connection { 20t .Helper () 21server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 22if r .URL .Path == "/server" { 23_ ,_ = w .Write ([]byte (testwire .Server )) 24return 25 } 26handler (w ,r ) 27 })) 28t .Cleanup (server .Close ) 29conn ,err := daemon .Attach (t .Context (), daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port ,Token :"token" },nil ) 30if err != nil { 31t .Fatal (err ) 32 } 33t .Cleanup (conn .HTTPClient ().CloseIdleConnections ) 34return conn 35} 36func awaitCommandSignal [T any ](t * testing.T ,ch <- chan T )T { 37t .Helper () 38select { 39case value := <- ch : 40return value 41case <- time .After (5 * time .Second ): 42t .Fatal ("timed out waiting for command" ) 43var zero T 44return zero 45 } 46} 47 48// A partial deletion must keep an undeleted row until an authoritative refresh. 49// A healthy daemon cannot reliably force a teardown failure during this update. 50func TestPartialDeletionKeepsUndeletedSessionAndOutcomeAfterRefresh (t * testing.T ) { 51app := NewAppModel (daemon .NewConnection (daemon.ConnectionSnapshot {Port :1 },nil ),Bootstrap {Settings : daemon.Settings {Profiles : config.Profiles {}}},nil ,"" ,false ,nil ) 52app .State = AppStateSessionPicker 53app .Sessions = []daemon.Session {{ID :"root" }, {ID :"deleted" }} 54app .updateSessionPickerItems () 55result := daemon.DeletionResult {State :"partial" ,Deleted :1 ,Remaining :1 ,DeletedIDs : []string {"deleted" },Message :"Deleted 1 sessions; 1 remain. root: kernel teardown failed" } 56updated ,command := app .Update (sessionDeletedMsg {ID :"root" ,Result :& result }) 57app = updated .(* AppModel ) 58if command == nil || len (app .Sessions )!= 1 || app .Sessions [0 ].ID != "root" || app .SessionPicker .notice != result .Message { 59t .Fatalf ("partial deletion invented completion: command nil=%v rows=%d id=%q notice=%q wanted=%q" ,command == nil ,len (app .Sessions ),app .Sessions [0 ].ID ,app .SessionPicker .notice ,result .Message ) 60 } 61updated ,_ = app .Update (sessionsLoadedMsg {Gen :app .SessionGen ,Sessions :app .Sessions ,Notice :result .Message }) 62app = updated .(* AppModel ) 63if app .SessionPicker .notice != result .Message { 64t .Fatal ("refresh erased the partial operation outcome" ) 65 } 66} 67func TestModelChangeCapturesProviderAndCapBeforeExecution (t * testing.T ) { 68for _ ,failure := range []string {"" ,"switch" ,"default" ,"cap" } { 69t .Run (failure ,func (t * testing.T ) { 70var paths []string 71var switchBody map [string ]string 72var capBody map [string ]map [string ]bool 73conn := commandTestConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 74paths = append (paths ,r .URL .String ()) 75if r .URL .Path == "/sessions/original" { 76if r .Header .Get ("If-Match" )!= "\"session-a\"" { 77t .Errorf ("changed session validator %s" ,r .Header .Get ("If-Match" )) 78 } 79_ = json .NewDecoder (r .Body ).Decode (& switchBody ) 80if failure == "switch" { 81w .WriteHeader (400 ) 82return 83 } 84writeSessionChange (w ,"original" ,"resolved" ,"new" ) 85return 86 } 87group := r .URL .Query ().Get ("group" ) 88if r .URL .Path != "/settings" { 89t .Errorf ("unexpected route %s" ,r .URL ) 90 } 91if failure == group || failure == "default" && group == "providers" || failure == "cap" && group == "models" { 92w .WriteHeader (400 ) 93return 94 } 95settings := testwire .Settings () 96if group == "models" { 97_ = json .NewDecoder (r .Body ).Decode (& capBody ) 98 } 99_ = json .NewEncoder (w ).Encode (testwire .SettingsChange (group ,settings [group ])) 100 }) 101session := & daemon.Session {ID :"original" ,Provider :"old" ,ETag :"\"session-a\"" } 102app := & AppModel {Conn :conn ,ActiveSession :session ,Profiles : config.Profiles {ETag :"\"providers-a\"" ,Providers :map [string ]config.Settings {"new" : {Protocol :"responses" }}},SettingsETags :map [string ]string {"models" :"\"models-a\"" }} 103raised := true 104cmd := app .changeModelCmd ("chosen" ,"new" ,"high" ,& raised ,7 ,"provider/cap" ) 105session .ID ,session .Provider = "changed" ,"new" 106raised = false 107app .Conn ,app .ActiveSession = nil ,nil 108msg := cmd ().(modelChangedMsg ) 109if msg .SessionID != "original" || msg .Gen != 7 || (msg .Err != nil )!= (failure != "" ) { 110t .Fatalf ("captured result changed: %+v" ,msg ) 111 } 112if ! reflect .DeepEqual (switchBody ,map [string ]string {"model" :"chosen" ,"provider_profile" :"new" ,"effort" :"high" }) { 113t .Fatalf ("switch inputs changed: %v" ,switchBody ) 114 } 115expected := 1 116if failure != "switch" { 117expected = 2 118if failure != "default" { 119expected = 3 120 } 121 } 122if len (paths )!= expected { 123t .Fatalf ("wrong partial workflow %v" ,paths ) 124 } 125if failure == "switch" { 126if msg .Selection != nil { 127t .Fatal ("failed switch installed" ) 128 } 129 }else if msg .Selection == nil || msg .Selection .Model != "resolved" { 130t .Fatal ("confirmed switch lost" ) 131 } 132if failure == "" && ! capBody ["raised_caps" ]["provider/cap" ] { 133t .Fatalf ("cap input changed %v" ,capBody ) 134 } 135 }) 136 } 137} 138func TestSessionCommandKeepsSessionWhileRequestIsRunning (t * testing.T ) { 139started ,release := make (chan struct {}),make (chan struct {}) 140var once sync.Once 141releaseRequest := func () {once .Do (func () {close (release ) }) } 142conn := commandTestConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 143if r .URL .Path != "/models" || r .URL .Query ().Get ("model" )!= "current" { 144t .Errorf ("wrong effort metadata %s" ,r .URL ) 145 } 146close (started ) 147<- release 148_ = json .NewEncoder (w ).Encode (map [string ]any {"items" : []any {testwire .Model ("current" )},"next" :nil }) 149 }) 150t .Cleanup (releaseRequest ) 151session := & daemon.Session {ID :"original" ,Model :"current" } 152app := & AppModel {Conn :conn ,ActiveSession :session } 153cmd := app .executeCommandCmd ("/effort" ,"" ,9 ) 154done := make (chan tea.Msg ,1 ) 155go func () {done <- cmd () }() 156awaitCommandSignal (t ,started ) 157session .ID = "changed" 158app .Conn ,app .ActiveSession = nil ,nil 159releaseRequest () 160msg := awaitCommandSignal (t ,done ).(commandExecutedMsg ) 161if msg .Err != nil || msg .SessionID != "original" || msg .Gen != 9 || ! reflect .DeepEqual (msg .Available , []string {"low" ,"high" }) { 162t .Fatalf ("result used changed app state %+v" ,msg ) 163 } 164} 165func TestStatusAndGlancesUseOneCapturedSessionSnapshot (t * testing.T ) { 166var paths []string 167conn := commandTestConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 168paths = append (paths ,r .URL .Path ) 169if r .URL .RawQuery != "tail=0" { 170t .Errorf ("state polling repeated history reads: %s" ,r .URL ) 171 } 172session := protocolSession ("original" ,generationA ,0 ) 173session ["kernel" ]= map [string ]any {"state" :"attached" ,"build" :"observed-build" ,"stale" :true ,"staleness_reasons" : []any {map [string ]any {"code" :"build_changed" ,"detail" :"A newer kernel is available." }},"live_job_count" :3 ,"running_jobs" : []any {map [string ]any {"id" :"job1" ,"pid" :123 ,"command" :"sleep 10" ,"started_at" :0 }},"stage" :nil ,"instance_id" :"observed-kernel" } 174session ["glances" ]= []any {map [string ]any {"extension" :"first" ,"title" :"First" ,"rows" : []any {},"url" :"/extensions/first/items" },map [string ]any {"extension" :"second" ,"title" :"Second" ,"rows" : []any {},"url" :"/extensions/second/items" }} 175_ = json .NewEncoder (w ).Encode (session ) 176 }) 177session := & daemon.Session {ID :"original" } 178chat := NewChatModel (session ,daemon .NewChatClient (conn ,session .ID )) 179t .Cleanup (chat .Close ) 180cmd := chat .statusCmd () 181session .ID = "changed" 182chat .SessionID = "changed" 183msg := cmd ().(ChatStatusMsg ) 184if ! reflect .DeepEqual (paths , []string {"/sessions/original" })|| msg .Err != nil || msg .SessionID != "original" || msg .Status == nil || len (msg .Glances )!= 2 { 185t .Fatalf ("glance capture changed %+v %v" ,msg ,paths ) 186 } 187chat .SessionID = "original" 188chat .Glances = []PageGlance {{Title :"confirmed" }} 189app := & AppModel {ActiveSession :& daemon.Session {ID :"original" },Chat :chat } 190updated ,refresh := app .Update (PageViewChangedMsg {}) 191app = updated .(* AppModel ) 192updated ,_ = app .Update (msg ) 193app = updated .(* AppModel ) 194if refresh == nil || len (app .Chat .Glances )!= 1 || app .Chat .Glances [0 ].Title != "confirmed" { 195t .Fatal ("an earlier state read overwrote the acknowledged page change" ) 196 } 197updated ,_ = app .Update (refresh ().(ChatStatusMsg )) 198app = updated .(* AppModel ) 199if len (app .Chat .Glances )!= 2 { 200t .Fatal ("the state read after the page change was discarded" ) 201 } 202phase := daemon .PhaseModel 203app .Chat .handleStreamEvent (daemon.StreamEvent {Type :"status" ,Status :& daemon.AgentStatus {Phase :& phase ,Running :true }}) 204status := app .Chat .Status 205if status .Phase == nil || * status .Phase != phase || ! status .Running || status .KernelLink != "attached" || ! status .KernelStale || status .KernelJobs == nil || * status .KernelJobs != 3 || len (status .RunningJobs )!= 1 || status .RunningJobs [0 ].Command != "sleep 10" || status .KernelBuild == nil || * status .KernelBuild != "observed-build" || status .KernelInstanceID == nil || * status .KernelInstanceID != "observed-kernel" { 206t .Fatalf ("lightweight live status lost the last captured kernel observation: %+v" ,status ) 207 } 208} 209func TestUIPatchCapturesValuesAndObservedValidator (t * testing.T ) { 210for _ ,sessionID := range []string {"" ,"original" } { 211t .Run (sessionID ,func (t * testing.T ) { 212var got map [string ]any 213conn := commandTestConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 214if r .Method != "PATCH" || r .Header .Get ("If-Match" )!= "\"seen\"" { 215t .Errorf ("conditional edit changed %s %v" ,r .Method ,r .Header ) 216 } 217_ = json .NewDecoder (r .Body ).Decode (& got ) 218if sessionID == "" { 219if r .URL .String ()!= "/settings?group=ui" { 220t .Errorf ("wrong route %s" ,r .URL ) 221 } 222_ = json .NewEncoder (w ).Encode (testwire .SettingsChange ("ui" ,testwire .Settings ()["ui" ])) 223 }else { 224if r .URL .String ()!= "/sessions/original?view=configuration" { 225t .Errorf ("wrong route %s" ,r .URL ) 226 } 227writeSessionChange (w ,"original" ,"current" ,"provider" ) 228 } 229 }) 230app := & AppModel {Conn :conn } 231app .UI .ETag = "\"seen\"" 232first ,second := true ,false 233patch := daemon.UIPreferencesPatch {Thinking :& first ,Tools :& second } 234if sessionID != "" { 235patch = daemon.UIPreferencesPatch {Pinned :& first ,Archived :& second ,ETag :"\"seen\"" } 236 } 237cmd := app .patchUICmd (sessionID ,patch ,5 ) 238first ,second = false ,true 239app .Conn = nil 240msg := cmd ().(uiSavedMsg ) 241if msg .Err != nil || msg .Gen != 5 { 242t .Fatalf ("patch failed %+v" ,msg ) 243 } 244if sessionID != "" { 245got = got ["preferences" ].(map [string ]any ) 246 } 247firstKey ,secondKey := "thinking" ,"tools" 248if sessionID != "" { 249firstKey ,secondKey = "pinned" ,"archived" 250 } 251if got [firstKey ]!= true || got [secondKey ]!= false { 252t .Fatalf ("captured values changed %v" ,got ) 253 } 254 }) 255 } 256} 257func TestPreviewCallbackKeepsConnectionAcrossAppChanges (t * testing.T ) { 258conn := commandTestConnection (t ,func (w http.ResponseWriter ,r * http.Request ) { 259if r .URL .String ()!= "/sessions/requested/history?limit=16" { 260t .Errorf ("unexpected preview %s" ,r .URL ) 261 } 262_ = json .NewEncoder (w ).Encode (map [string ]any {"high_water" :1 ,"items" : []any {protocolEntry ("e" ,"user" ,"hello" ,1 )},"older" :nil ,"newer" :nil }) 263 }) 264app := NewAppModel (conn ,Bootstrap {Settings : daemon.Settings {Profiles : config.Profiles {}}},nil ,"" ,false ,nil ) 265app .Conn = nil 266msg := app .SessionPicker .Fetch ("requested" )().(SessionPreviewMsg ) 267if msg .Err != nil || msg .ID != "requested" || len (msg .Preview .Items )!= 1 || msg .Preview .Items [0 ].Preview != "hello" { 268t .Fatalf ("preview followed changed app state %+v" ,msg ) 269 } 270}