okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
5b3f8be
main
1//go:build unix 2 3// Slash shortcuts must change the same real resources as their page actions, 4// retaining workspace boundaries, observed validators, and delete confirmation. 5package e2e 6 7import ( 8"encoding/json" 9"errors" 10"net/http" 11"slices" 12"strings" 13"testing" 14 15"albedo/cli/internal/daemon" 16"albedo/cli/internal/daemon/protocol" 17"albedo/cli/internal/tui" 18 19 tea"charm.land/bubbletea/v2" 20) 21 22func executeShortcut (t * testing.T ,session ,command ,arguments string ) daemon.CommandResult { 23t .Helper () 24prepared ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session ,command ,arguments ) 25if err != nil { 26t .Fatalf ("prepare %s %s: %v" ,command ,arguments ,err ) 27 } 28result ,err := daemon .ExecutePageAction (t .Context (),conn (t ),prepared .Request ) 29if err != nil { 30t .Fatalf ("execute %s %s: %v" ,command ,arguments ,err ) 31 } 32return result 33} 34 35func shortcutWorkItem (t * testing.T ,result daemon.CommandResult ) protocol.WorkItem { 36t .Helper () 37var changed protocol.WorkChange 38if err := json .Unmarshal (result .Result ,& changed );err != nil || changed .Resource .Value .ID == "" { 39t .Fatalf ("work change: %s, %v" ,result .Result ,err ) 40 } 41return changed .Resource .Value 42} 43 44func shortcutPaperclip (t * testing.T ,result daemon.CommandResult ) protocol.Paperclip { 45t .Helper () 46var changed protocol.PaperclipChange 47if err := json .Unmarshal (result .Result ,& changed );err != nil || changed .Resource .Value .ID == "" { 48t .Fatalf ("paperclip change: %s, %v" ,result .Result ,err ) 49 } 50return changed .Resource .Value 51} 52 53func createShortcutPaperclip (t * testing.T ,session string ) protocol.Paperclip { 54t .Helper () 55page ,err := daemon .LoadPage (t .Context (),conn (t ),session ,"/paperclips" ) 56if err != nil { 57t .Fatal (err ) 58 } 59index := slices .IndexFunc (page .Actions ,func (action daemon.PageAction )bool {return action .ID == "create" }) 60if index < 0 { 61t .Fatal ("paperclips page has no create action" ) 62 } 63result ,err := daemon .ExecutePageAction (t .Context (),conn (t ), daemon.PageActionRequest { 64Action :page .Actions [index ],Session :page .Session , 65Form :map [string ]json.RawMessage {"message" :json .RawMessage (`"shortcut fixture"` )}, 66 }) 67if err != nil { 68t .Fatal (err ) 69 } 70return shortcutPaperclip (t ,result ) 71} 72 73func TestWorkShortcutsPreserveOtherFieldsAndWorkspaceScope (t * testing.T ) { 74providerRoute (t ,echoReply ) 75t .Parallel () 76session := newSession (t ,t .TempDir ()) 77item := shortcutWorkItem (t ,executeShortcut (t ,session ,"/work" ,"add a multiword title" )) 78if item .Title != "a multiword title" || item .Status != "open" { 79t .Fatalf ("add shortcut changed the wrong fields: %+v" ,item ) 80 } 81prepared ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session ,"/work" ,"edit " + item .ID + " revised title" ) 82if err != nil { 83t .Fatal (err ) 84 } 85prepared .Request .Form ["notes" ]= json .RawMessage (`"keep these notes"` ) 86result ,err := daemon .ExecutePageAction (t .Context (),conn (t ),prepared .Request ) 87if err != nil { 88t .Fatal (err ) 89 } 90item = shortcutWorkItem (t ,result ) 91if item .Title != "revised title" || item .Notes != "keep these notes" { 92t .Fatalf ("edit shortcut: %+v" ,item ) 93 } 94item = shortcutWorkItem (t ,executeShortcut (t ,session ,"/work" ,"status " + item .ID + " blocked" )) 95if item .Status != "blocked" || item .Title != "revised title" || item .Notes != "keep these notes" { 96t .Fatalf ("status shortcut lost the other fields: %+v" ,item ) 97 } 98item = shortcutWorkItem (t ,executeShortcut (t ,session ,"/work" ,"edit " + item .ID + " final title" )) 99if item .Title != "final title" || item .Status != "blocked" || item .Notes != "keep these notes" { 100t .Fatalf ("title shortcut lost the other fields: %+v" ,item ) 101 } 102other := newSession (t ,t .TempDir ()) 103_ ,err = daemon .PreparePageShortcut (t .Context (),conn (t ),other ,"/work" ,"edit " + item .ID + " crossed workspace" ) 104problem ,ok := errors.AsType [* daemon.APIError ](err ) 105if ! ok || problem .StatusCode != http .StatusNotFound { 106t .Fatalf ("shortcut reached another workspace's item: %v" ,err ) 107 } 108unchanged ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session ,"/work" ,"edit " + item .ID + " untouched" ) 109if err != nil || unchanged .Request .Row .Text != "final title" { 110t .Fatalf ("foreign workspace shortcut changed the item: %+v, %v" ,unchanged ,err ) 111 } 112if status := daemonSession (t ,session ).Status ;status .KernelInstanceID != nil { 113t .Fatalf ("HTTP page shortcuts opened a kernel: %+v" ,status ) 114 } 115} 116 117func TestPaperclipShortcutsApplyDeclaredStatusAndReplyActions (t * testing.T ) { 118providerRoute (t ,echoReply ) 119t .Parallel () 120session := newSession (t ,t .TempDir ()) 121item := createShortcutPaperclip (t ,session ) 122for _ ,scenario := range []struct { 123arguments ,status ,reply ,resolution string 124 }{ 125 {"acknowledge " + item .ID ,"acknowledged" ,"" ,"" }, 126 {"reply " + item .ID + " a multiword reply" ,"acknowledged" ,"a multiword reply" ,"" }, 127 {"resolve " + item .ID + " a multiword resolution" ,"resolved" ,"a multiword reply" ,"a multiword resolution" }, 128 {"dismiss " + item .ID ,"dismissed" ,"a multiword reply" ,"a multiword resolution" }, 129 {"resolve " + item .ID ,"resolved" ,"a multiword reply" ,"a multiword resolution" }, 130 } { 131item = shortcutPaperclip (t ,executeShortcut (t ,session ,"/paperclips" ,scenario .arguments )) 132if item .Status != scenario .status || item .Reply != scenario .reply || item .Resolution != scenario .resolution { 133t .Fatalf ("%s changed the wrong fields: %+v" ,scenario .arguments ,item ) 134 } 135 } 136} 137 138func TestPageShortcutsRejectInvalidArgumentsWithoutChangingResources (t * testing.T ) { 139providerRoute (t ,echoReply ) 140t .Parallel () 141session := newSession (t ,t .TempDir ()) 142work := shortcutWorkItem (t ,executeShortcut (t ,session ,"/work" ,"add unchanged work" )) 143clip := createShortcutPaperclip (t ,session ) 144for _ ,scenario := range []struct {command ,arguments string }{ 145 {"/work" ,"add" }, 146 {"/work" ,"edit" }, 147 {"/work" ,"edit 0 title" }, 148 {"/work" ,"edit invalid title" }, 149 {"/work" ,"status " + work .ID }, 150 {"/work" ,"status " + work .ID + " unsupported" }, 151 {"/work" ,"remove " + work .ID + " extra" }, 152 {"/work" ,"unknown " + work .ID }, 153 {"/paperclips" ,"reply " + clip .ID }, 154 {"/paperclips" ,"acknowledge" }, 155 {"/paperclips" ,"dismiss " + clip .ID + " extra" }, 156 {"/paperclips" ,"remove -1" }, 157 } { 158if _ ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session ,scenario .command ,scenario .arguments );err == nil { 159t .Fatalf ("accepted invalid shortcut %s %s" ,scenario .command ,scenario .arguments ) 160 } 161 } 162for _ ,scenario := range []struct {command ,arguments ,title ,badge string }{ 163 {"/work" ,"edit " + work .ID + " unsubmitted" ,"unchanged work" ,"open" }, 164 {"/paperclips" ,"acknowledge " + clip .ID ,"shortcut fixture" ,"open" }, 165 } { 166prepared ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session ,scenario .command ,scenario .arguments ) 167if err != nil || prepared .Request .Row .Text != scenario .title || prepared .Request .Row .Badge != scenario .badge { 168t .Fatalf ("invalid arguments changed a resource: %+v, %v" ,prepared ,err ) 169 } 170 } 171} 172 173func TestWorkShortcutFindsOffPageTargetAndRejectsStaleValidator (t * testing.T ) { 174providerRoute (t ,echoReply ) 175t .Parallel () 176session := newSession (t ,t .TempDir ()) 177creation ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session ,"/work" ,"add paginated fixture" ) 178if err != nil { 179t .Fatal (err ) 180 } 181var target protocol.WorkItem 182for range 51 { 183result ,err := daemon .ExecutePageAction (t .Context (),conn (t ),creation .Request ) 184if err != nil { 185t .Fatal (err ) 186 } 187target = shortcutWorkItem (t ,result ) 188 } 189page ,err := daemon .LoadPage (t .Context (),conn (t ),session ,"/work" ) 190if err != nil { 191t .Fatal (err ) 192 } 193if slices .ContainsFunc (page .Rows ,func (row daemon.PageRow )bool {return row .ID == target .ID }) { 194t .Fatal ("fixture did not place its target beyond the first page" ) 195 } 196stale ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session ,"/work" ,"edit " + target .ID + " stale title" ) 197if err != nil || stale .Request .Row .ID != target .ID || ! slices .ContainsFunc (stale .Page .Rows ,func (row daemon.PageRow )bool {return row .ID == target .ID }) { 198t .Fatalf ("off-page target was not captured: %+v, %v" ,stale ,err ) 199 } 200target = shortcutWorkItem (t ,executeShortcut (t ,session ,"/work" ,"edit " + target .ID + " newer title" )) 201_ ,err = daemon .ExecutePageAction (t .Context (),conn (t ),stale .Request ) 202problem ,ok := errors.AsType [* daemon.APIError ](err ) 203if ! ok || problem .StatusCode != http .StatusPreconditionFailed { 204t .Fatalf ("stale shortcut overwrote a newer edit: %v" ,err ) 205 } 206current ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session ,"/work" ,"edit " + target .ID + " unsubmitted" ) 207if err != nil || current .Request .Row .Text != "newer title" { 208t .Fatalf ("stale edit changed the item: %+v, %v" ,current ,err ) 209 } 210} 211 212func TestTUIPageRemoveShortcutConfirmsOrCancels (t * testing.T ) { 213providerRoute (t ,echoReply ) 214t .Parallel () 215session := daemonSession (t ,newSession (t ,t .TempDir ())) 216driver := driveTUI (t ,& session ) 217t .Cleanup (func () {driver .App .Chat .Close () }) 218driver .connected () 219commands ,err := daemon .ListSessionCommands (t .Context (),conn (t ),session .ID ) 220if err != nil { 221t .Fatal (err ) 222 } 223driver .App .Chat .CommandMenu .Catalog = commands 224driver .App .Chat .TextArea .SetValue ("/work add confirmed work removal" ) 225driver .Dispatch (tea.KeyPressMsg {Code :tea .KeyEnter }) 226if driver .App .State != tui .AppStatePageView || driver .App .PageView .Error != "" || driver .App .PageView .Doc == nil || len (driver .App .PageView .Doc .Rows )!= 1 { 227t .Fatalf ("composer shortcut did not create a work item:\n%s" ,driver .View ()) 228 } 229workID := driver .App .PageView .Doc .Rows [0 ].ID 230clip := createShortcutPaperclip (t ,session .ID ) 231for _ ,scenario := range []struct {command ,id string }{{"/work" ,workID }, {"/paperclips" ,clip .ID }} { 232remove := tui.ChatExecuteCommandMsg {Name :scenario .command ,Args :"remove " + scenario .id } 233driver .Dispatch (remove ) 234if driver .App .State != tui .AppStatePageView || driver .App .PageView .CurrentAction == nil || ! driver .App .PageView .CurrentAction .Confirm || ! strings .Contains (driver .View (),"Delete this item?" ) { 235t .Fatalf ("remove skipped confirmation for %s:\n%s" ,scenario .command ,driver .View ()) 236 } 237if _ ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session .ID ,scenario .command ,"remove " + scenario .id );err != nil { 238t .Fatalf ("item disappeared before confirmation: %v" ,err ) 239 } 240driver .Dispatch (tea.KeyPressMsg {Code :tea .KeyEscape }) 241if driver .App .PageView .CurrentAction != nil { 242t .Fatal ("cancelled removal retained its action" ) 243 } 244if _ ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session .ID ,scenario .command ,"remove " + scenario .id );err != nil { 245t .Fatalf ("Escape removed the item: %v" ,err ) 246 } 247driver .Dispatch (remove ) 248driver .Dispatch (tea.KeyPressMsg {Code :tea .KeyEnter }) 249if driver .App .PageView .Error != "" { 250t .Fatalf ("confirmed removal failed: %s" ,driver .App .PageView .Error ) 251 } 252_ ,err := daemon .PreparePageShortcut (t .Context (),conn (t ),session .ID ,scenario .command ,"remove " + scenario .id ) 253problem ,ok := errors.AsType [* daemon.APIError ](err ) 254if ! ok || problem .StatusCode != http .StatusNotFound { 255t .Fatalf ("confirmed %s item %s still exists: %v" ,scenario .command ,scenario .id ,err ) 256 } 257 } 258}