okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1// The picker's keyboard state machine (search versus browsing, per-row effort 2// picks, cap toggles) needs catalog shapes the e2e fake provider cannot serve, 3// so these tests check what enter sends for each state. 4package tui 5 6import ( 7"strings" 8"testing" 9 10"albedo/cli/internal/config" 11"albedo/cli/internal/daemon" 12 13 tea"charm.land/bubbletea/v2" 14"github.com/charmbracelet/x/ansi" 15) 16 17var pickerProfiles = config.Profiles { 18Active :"work" , 19Providers :map [string ]config.Settings { 20"work" : {Extension :"openai" ,BaseURL :"https://work.example/v1" ,Model :"gpt-5" ,Protocol :"responses" }, 21"codex" : {Extension :"codex" ,Model :"gpt-5-codex" ,Protocol :"responses" }, 22 }, 23} 24 25var reasoning = []string {"low" ,"medium" ,"high" } 26 27func loadedPicker (t * testing.T )ModelPickerModel { 28t .Helper () 29m := NewModelPickerModel (nil ,pickerProfiles ,"gpt-5" ,"work" ,"high" ) 30m .SetSize (120 ,30 ) 31m ,_ = m .Update (modelCatalogLoadedMsg {Generation :m .Generation ,Profile :"work" ,Models : []daemon.Model { 32 {ID :"gpt-4.1" ,Context :1_047_576 }, 33 {ID :"gpt-5" ,Efforts :reasoning ,Context :400_000 ,Output :128_000 ,Input : []string {"text" ,"image" }}, 34 {ID :"o4-mini" ,Efforts : []string {"low" ,"medium" }}, 35 }}) 36m ,_ = m .Update (modelCatalogLoadedMsg {Generation :m .Generation ,Profile :"codex" ,Models : []daemon.Model { 37 {ID :"gpt-5-codex" ,Efforts : []string {"minimal" ,"low" ,"medium" ,"high" }}, 38 }}) 39return m 40} 41 42func pickerKey (m ModelPickerModel ,keys ... tea.KeyPressMsg )ModelPickerModel { 43for _ ,k := range keys { 44m ,_ = m .Update (k ) 45 } 46return m 47} 48 49func typed (s string ) tea.KeyPressMsg {return tea.KeyPressMsg {Code :tea .KeyExtended ,Text :s } } 50 51func chosen (t * testing.T ,m ModelPickerModel )ModelPickerSelectMsg { 52t .Helper () 53_ ,cmd := m .Update (tea.KeyPressMsg {Code :tea .KeyEnter }) 54if cmd == nil { 55t .Fatal ("enter chose nothing" ) 56 } 57msg ,ok := cmd ().(ModelPickerSelectMsg ) 58if ! ok { 59t .Fatalf ("enter sent %T" ,cmd ()) 60 } 61return msg 62} 63 64func rowIDs (m ModelPickerModel ) []string { 65ids := make ([]string ,len (m .rows )) 66for i ,r := range m .rows { 67ids [i ]= r .profile + "/" + r .model .ID 68if r .typed { 69ids [i ]= "typed:" + ids [i ] 70 } 71 } 72return ids 73} 74 75func TestModelPickerArrowsStepEffortAndEnterSendsIt (t * testing.T ) { 76m := loadedPicker (t ) 77if got := chosen (t ,m );got .Effort != "high" || got .Model != "gpt-5" || got .Provider != "work" { 78t .Fatalf ("unchanged choice %+v, want the session's high effort" ,got ) 79 } 80m = pickerKey (m , tea.KeyPressMsg {Code :tea .KeyLeft }, tea.KeyPressMsg {Code :tea .KeyLeft }, tea.KeyPressMsg {Code :tea .KeyLeft }) 81if got := chosen (t ,m );got .Effort != "low" { 82t .Fatalf ("effort %q after stepping down past the end, want low" ,got .Effort ) 83 } 84m = pickerKey (m , tea.KeyPressMsg {Code :tea .KeyRight }) 85if got := chosen (t ,m );got .Effort != "medium" { 86t .Fatalf ("effort %q, want medium" ,got .Effort ) 87 } 88// Another row lacking the session's level shows the daemon's default, 89// and the first row keeps its pick. 90m = pickerKey (m , tea.KeyPressMsg {Code :tea .KeyDown }, tea.KeyPressMsg {Code :tea .KeyDown }) 91if got := chosen (t ,m );got .Model != "o4-mini" || got .Effort != "medium" { 92t .Fatalf ("choice %+v, want o4-mini at medium" ,got ) 93 } 94m = pickerKey (m , tea.KeyPressMsg {Code :tea .KeyUp }, tea.KeyPressMsg {Code :tea .KeyUp }) 95if got := chosen (t ,m );got .Effort != "medium" { 96t .Fatalf ("first row forgot its pick: %+v" ,got ) 97 } 98// A model without levels sends none. 99m = pickerKey (m , tea.KeyPressMsg {Code :tea .KeyDown }, tea.KeyPressMsg {Code :tea .KeyRight }) 100if got := chosen (t ,m );got .Model != "gpt-4.1" || got .Effort != "" { 101t .Fatalf ("choice %+v, want gpt-4.1 without effort" ,got ) 102 } 103} 104 105func TestModelPickerArrowsEditASearchUntilYouBrowse (t * testing.T ) { 106m := loadedPicker (t ) 107m = pickerKey (m ,typed ("codex" )) 108if got := strings .Join (rowIDs (m )," " );got != "codex/gpt-5-codex typed:work/codex typed:codex/codex" { 109t .Fatalf ("search rows %q" ,got ) 110 } 111m = pickerKey (m , tea.KeyPressMsg {Code :tea .KeyLeft },typed ("x" )) 112if m .search .Value ()!= "codexx" { 113t .Fatalf ("left arrow did not reach the search: %q" ,m .search .Value ()) 114 } 115m = pickerKey (m , tea.KeyPressMsg {Code :tea .KeyBackspace }, tea.KeyPressMsg {Code :tea .KeyUp }, tea.KeyPressMsg {Code :tea .KeyLeft }) 116if got := chosen (t ,m );got .Model != "gpt-5-codex" || got .Effort != "medium" { 117t .Fatalf ("after browsing, left should lower high to medium: %+v" ,got ) 118 } 119} 120 121func TestModelPickerOffersATypedIDToEveryProfileMissingIt (t * testing.T ) { 122m := pickerKey (loadedPicker (t ),typed ("gpt-5-codex" )) 123got := strings .Join (rowIDs (m )," " ) 124if got != "codex/gpt-5-codex typed:work/gpt-5-codex" { 125t .Fatalf ("rows %q" ,got ) 126 } 127m = pickerKey (m , tea.KeyPressMsg {Code :tea .KeyDown }) 128if got := chosen (t ,m );got .Model != "gpt-5-codex" || got .Provider != "work" || got .Effort != "" { 129t .Fatalf ("typed choice %+v" ,got ) 130 } 131} 132 133func TestModelPickerViewFitsEveryWidth (t * testing.T ) { 134for _ ,size := range [][2 ]int {{30 ,10 }, {60 ,20 }, {80 ,24 }, {100 ,30 }, {160 ,40 }} { 135m := loadedPicker (t ) 136m .SetSize (size [0 ],size [1 ]) 137view := m .View () 138lines := strings .Split (view ,"\n" ) 139if len (lines )> size [1 ] { 140t .Fatalf ("%v: %d lines" ,size ,len (lines )) 141 } 142for _ ,line := range lines { 143if w := ansi .StringWidth (line );w > size [0 ] { 144t .Fatalf ("%v: line is %d cells wide: %q" ,size ,w ,ansi .Strip (line )) 145 } 146 } 147 } 148} 149 150func TestModelPickerTabRaisesAModelsContextCap (t * testing.T ) { 151m := NewModelPickerModel (nil ,pickerProfiles ,"gpt-5-codex" ,"codex" ,"" ) 152m .SetSize (140 ,30 ) 153m ,_ = m .Update (modelCatalogLoadedMsg {Generation :m .Generation ,Profile :"codex" ,Models : []daemon.Model { 154 {ID :"gpt-6-astra" ,Context :272_000 ,MaxContext :872_000 ,Efforts :reasoning }, 155 {ID :"gpt-6-sol" ,CapKey :"codex/gpt-6-sol" ,Context :272_000 ,MaxContext :872_000 ,Raised :true }, 156 {ID :"gpt-5.5" ,Context :272_000 }, 157 }}) 158m = pickerKey (m ,typed ("astra" )) 159if view := ansi .Strip (m .View ());! strings .Contains (view ,"tab raises to 872k" )|| ! strings .Contains (view ,"272k tokens" ) { 160t .Fatalf ("astra should offer to raise its cap:\n%s" ,view ) 161 } 162m = pickerKey (m , tea.KeyPressMsg {Code :tea .KeyTab }) 163if view := ansi .Strip (m .View ());! strings .Contains (view ,"872k tokens" )|| ! strings .Contains (view ,"tab restores 272k" ) { 164t .Fatalf ("a raised cap should show the larger window:\n%s" ,view ) 165 } 166if got := chosen (t ,m ).RaiseCap ;got == nil || !* got { 167t .Fatalf ("enter should raise astra's cap, got %v" ,got ) 168 } 169// Flipping back leaves the saved cap alone. 170m = pickerKey (m , tea.KeyPressMsg {Code :tea .KeyTab }) 171if got := chosen (t ,m ).RaiseCap ;got != nil { 172t .Fatalf ("an unchanged cap should send nothing, got %v" ,* got ) 173 } 174 175// A saved raise shows as raised, and tab restores the default. 176sol := pickerKey (m , tea.KeyPressMsg {Code :tea .KeyBackspace }, tea.KeyPressMsg {Code :tea .KeyBackspace }, 177 tea.KeyPressMsg {Code :tea .KeyBackspace }, tea.KeyPressMsg {Code :tea .KeyBackspace }, 178 tea.KeyPressMsg {Code :tea .KeyBackspace },typed ("6-sol" )) 179if view := ansi .Strip (sol .View ());! strings .Contains (view ,"tab restores 272k" ) { 180t .Fatalf ("sol's saved raise should show:\n%s" ,view ) 181 } 182sol = pickerKey (sol , tea.KeyPressMsg {Code :tea .KeyTab }) 183if got := chosen (t ,sol ).RaiseCap ;got == nil || * got { 184t .Fatalf ("tab should restore sol's default window, got %v" ,got ) 185 } 186 187// Nothing to raise, nothing to toggle. 188older := pickerKey (sol , tea.KeyPressMsg {Code :tea .KeyBackspace }, tea.KeyPressMsg {Code :tea .KeyBackspace }, 189 tea.KeyPressMsg {Code :tea .KeyBackspace }, tea.KeyPressMsg {Code :tea .KeyBackspace }, 190 tea.KeyPressMsg {Code :tea .KeyBackspace },typed ("5.5" ), tea.KeyPressMsg {Code :tea .KeyTab }) 191if view := ansi .Strip (older .View ());strings .Contains (view ,"tab raises" )|| strings .Contains (view ,"tab cap" ) { 192t .Fatalf ("gpt-5.5 has no cap to raise:\n%s" ,view ) 193 } 194if got := chosen (t ,older ).RaiseCap ;got != nil { 195t .Fatalf ("gpt-5.5 should send no cap, got %v" ,* got ) 196 } 197}