okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
2ecf203
main
1// Screen replacement and modal races depend on command delivery order, which 2// daemon E2E cannot control. These tests drive the actual Bubble Tea models. 3package tui 4 5import ( 6"albedo/cli/internal/daemon" 7"errors" 8"strings" 9"testing" 10 11"charm.land/bubbles/v2/textinput" 12 tea"charm.land/bubbletea/v2" 13) 14 15func TestReopenedScreensRejectPreviousRequestReplies (t * testing.T ) { 16for _ ,replyErr := range []error {nil ,errors .New ("discarded request failed" )} { 17t .Run ("page" ,func (t * testing.T ) { 18old := NewPageViewModel (nil ,"s" ,"/notes" ) 19old ,_ = old .Update (pageLoadedMsg {Gen :old .Generation ,Err :errors .New ("retry" )}) 20old ,_ = old .Update (tea.KeyPressMsg {Code :'r' ,Text :"r" }) 21current := NewPageViewModel (nil ,"s" ,"/notes" ) 22current ,cmd := current .Update (pageLoadedMsg {Gen :old .Generation ,Err :replyErr ,Doc :& PageDocument {Title :"obsolete" }}) 23if ! current .Busy || current .Doc != nil || current .Error != "" || cmd != nil { 24t .Fatal ("old load changed reopened page" ) 25 } 26current ,cmd = current .Update (pageActionExecutedMsg {Gen :old .Generation ,Err :replyErr }) 27if ! current .Busy || current .Error != "" || cmd != nil { 28t .Fatal ("old action completed reopened page" ) 29 } 30current ,_ = current .Update (pageLoadedMsg {Gen :current .Generation ,Doc :& PageDocument {Title :"current" }}) 31if current .Busy || current .Doc .Title != "current" { 32t .Fatal ("current load was rejected" ) 33 } 34 }) 35t .Run ("extensions" ,func (t * testing.T ) { 36old := NewExtensionPickerModel (nil ,"s" ) 37old ,_ = old .Update (extensionsLoadedMsg {Gen :old .Generation ,Err :errors .New ("retry" )}) 38old ,_ = old .Update (tea.KeyPressMsg {Code :'r' ,Text :"r" }) 39current := NewExtensionPickerModel (nil ,"s" ) 40current .Saving = true 41current ,_ = current .Update (extensionsLoadedMsg {Gen :old .Generation ,Err :replyErr ,Extensions : []ExtensionItem {{Name :"obsolete" }}}) 42current ,cmd := current .Update (extensionToggledMsg {Gen :old .Generation ,Err :replyErr }) 43if ! current .Loading || ! current .Saving || len (current .Extensions )!= 0 || current .Error != "" || cmd != nil { 44t .Fatal ("old reply changed reopened extensions" ) 45 } 46 }) 47t .Run ("tree" ,func (t * testing.T ) { 48old := NewTreePickerModel (nil ,"s" ) 49old ,_ = old .loadPage (10 ) 50current := NewTreePickerModel (nil ,"s" ) 51current .Forking = true 52current ,_ = current .Update (treeLoadedMsg {Gen :old .Generation ,Err :replyErr }) 53current ,cmd := current .Update (treeForkedMsg {Gen :old .Generation ,Err :replyErr }) 54if ! current .Loading || ! current .Forking || current .Error != "" || current .ForkError != "" || cmd != nil { 55t .Fatal ("old reply changed reopened tree" ) 56 } 57 }) 58t .Run ("context" ,func (t * testing.T ) { 59old := NewContextInspectorModel (nil ,"s" ) 60old ,_ = old .Update (tea.KeyPressMsg {Code :'r' ,Mod :tea .ModCtrl }) 61current := NewContextInspectorModel (nil ,"s" ) 62current .Detail = & ContextDetail {Section : daemon.ContextSection {ID :"history" }} 63current ,_ = current .Update (contextSnapshotLoadedMsg {Gen :old .Generation ,Err :replyErr ,Snapshot :& daemon.ContextSnapshot {State :"ready" }}) 64current ,_ = current .Update (contextPageLoadedMsg {Gen :old .Generation ,Err :replyErr ,SectionID :"history" ,Data :& daemon.ContextPage {Content :"obsolete" }}) 65if ! current .Loading || current .Snapshot != nil || current .Error != "" || current .Detail .Value != nil || current .Detail .Error != "" { 66t .Fatal ("old reply changed reopened context" ) 67 } 68 }) 69t .Run ("webhooks" ,func (t * testing.T ) { 70old := NewWebhooksPageModel (nil ,"s" ) 71old .Loading = false 72old ,_ = old .Update (tea.KeyPressMsg {Code :'r' ,Mod :tea .ModCtrl }) 73current := NewWebhooksPageModel (nil ,"s" ) 74current .Saving = true 75current ,_ = current .Update (webhooksLoadedMsg {Gen :old .Generation ,Err :replyErr }) 76current ,cmd := current .Update (webhooksSavedMsg {Gen :old .Generation ,Err :replyErr ,Notice :"obsolete" }) 77if ! current .Loading || ! current .Saving || current .Error != "" || current .Notice != "" || cmd != nil { 78t .Fatal ("old reply changed reopened webhooks" ) 79 } 80 }) 81 } 82} 83 84func TestPageFocusedInputReceivesPasteAndCursorCommands (t * testing.T ) { 85for _ ,inputKind := range []string {"text" ,"secret" } { 86t .Run (inputKind ,func (t * testing.T ) { 87page := NewPageViewModel (nil ,"s" ,"/notes" ) 88page .Busy = false 89page .setDoc (& PageDocument {Actions : []PageAction {{Key :"ctrl+e" ,Label :"edit" ,Fields : []daemon.FormField {{Name :"value" ,Label :"value" ,Type :inputKind }}}}}) 90styles := page .TextInput .Styles () 91styles .Cursor .Blink = true 92page .TextInput .SetStyles (styles ) 93page ,_ = page .Update (tea.KeyPressMsg {Code :'e' ,Mod :tea .ModCtrl }) 94page ,_ = page .Update (tea.PasteMsg {Content :"pasted value" }) 95if page .TextInput .Value ()!= "pasted value" { 96t .Fatal ("bracketed paste was lost" ) 97 } 98if inputKind == "secret" && strings .Contains (page .View (),"pasted value" ) { 99t .Fatal ("pasted secret appeared in view" ) 100 } 101page ,cmd := page .Update (textinput .Blink ()) 102if cmd == nil { 103t .Fatal ("input cursor command was dropped" ) 104 } 105page ,_ = page .Update (tea.KeyPressMsg {Code :tea .KeyEscape }) 106page ,_ = page .Update (tea.PasteMsg {Content :"ignored" }) 107if page .TextInput .Value ()!= "" { 108t .Fatal ("dismissed form retained input or browse mode accepted paste" ) 109 } 110 }) 111 } 112} 113 114func TestChatHistoryAndWindowCompletionsSurviveModal (t * testing.T ) { 115conn := daemon .NewConnection (daemon.ConnectionSnapshot {Port :1 },nil ) 116session := & daemon.Session {ID :"s" } 117chat := NewChatModel (session ,daemon .NewChatClient (conn ,"s" )) 118defer chat .Close () 119chat .SetSize (80 ,24 ) 120chat .loadingOlder = true 121model := & AppModel {ActiveSession :session ,Chat :chat ,State :AppStateContextInspector } 122updated ,_ := model .Update (ChatOlderLoadedMsg {SessionID :"other" ,Generation :chat .Generation ,Page :& daemon.HistoryPage {}}) 123model = updated .(* AppModel ) 124if ! model .Chat .loadingOlder { 125t .Fatal ("another session completed the active history request" ) 126 } 127updated ,_ = model .Update (ChatOlderLoadedMsg {SessionID :"s" ,Generation :chat .Generation - 1 ,Page :& daemon.HistoryPage {}}) 128model = updated .(* AppModel ) 129if ! model .Chat .loadingOlder { 130t .Fatal ("stale generation completed the active history request" ) 131 } 132updated ,_ = model .Update (ChatOlderLoadedMsg {SessionID :"s" ,Generation :chat .Generation ,Page :& daemon.HistoryPage {Before :10 ,More :true }}) 133model = updated .(* AppModel ) 134if model .Chat .loadingOlder || ! model .Chat .olderMore { 135t .Fatal ("modal lost history completion" ) 136 } 137tokens := 123456 138updated ,_ = model .Update (ChatWindowMsg {SessionID :"s" ,Generation :chat .Generation ,Model :"model" ,Tokens :& tokens }) 139model = updated .(* AppModel ) 140if model .Chat .window == nil || * model .Chat .window != tokens { 141t .Fatal ("modal lost context window completion" ) 142 } 143model .Chat .Follow = false 144if model .Chat .loadOlder ()== nil || ! model .Chat .loadingOlder { 145t .Fatal ("paging stayed stuck after modal completion" ) 146 } 147} 148 149func TestDelayedForkCompletionCannotReplaceReopenedTree (t * testing.T ) { 150old := NewTreePickerModel (nil ,"s" ) 151old .Forking = true 152old ,cmd := old .Update (treeForkedMsg {Gen :old .Generation ,Session : daemon.Session {ID :"fork" }}) 153if old .Forking || cmd == nil { 154t .Fatal ("current fork did not complete" ) 155 } 156model := & AppModel {State :AppStateTreePicker ,TreePicker :NewTreePickerModel (nil ,"s" ),ActiveSession :& daemon.Session {ID :"s" }} 157updated ,next := model .Update (cmd ()) 158model = updated .(* AppModel ) 159if model .ActiveSession .ID != "s" || next != nil { 160t .Fatal ("discarded tree completion changed session" ) 161 } 162}