okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
f65a648
main
1// Action-row lifetime across streaming, replay, and turn completion is not exercised by daemon E2E. 2package tui 3 4import ( 5"strings" 6"testing" 7 8"albedo/cli/internal/daemon" 9"albedo/cli/internal/daemon/protocol" 10 tea"charm.land/bubbletea/v2" 11"github.com/charmbracelet/x/ansi" 12) 13 14func TestGroupedActionRowHoldsUntilTheNextAction (t * testing.T ) { 15m := newTestChatModel (t ,& daemon.Session {ID :"s" }) 16m .SetSize (100 ,30 ) 17m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventUser ,Text :"check it" }) 18progress := func (target string ) { 19m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress , 20Progress :& daemon.ToolProgress {CallID :"progress-" + target ,Name :"python" ,Phase :"running" }}) 21 } 22finish := func (kind ,target string ) { 23m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventTool ,ToolName :"python" , 24ProgressCallID :"progress-" + target , 25ToolResult :`{"status":"ok"}` ,ToolTrace :& daemon.ToolTrace {Activities : []daemon.ToolActivity { 26 {Kind :kind ,Target :target }, 27 }}}) 28 } 29visible := func ()string { 30m .refreshViewportContent () 31return ansi .Strip (m .Viewport .View ()) 32 } 33occupied := func ()int { 34n := 0 35for row := range strings .SplitSeq (visible (),"\n" ) { 36if strings .TrimSpace (row )!= "" { 37n ++ 38 } 39 } 40return n 41 } 42 43progress ("first.go" ) 44finish ("read" ,"first.go" ) 45if got := visible ();! strings .Contains (got ,"read first.go" ) { 46t .Fatalf ("completed read vanished: %q" ,got ) 47 } 48 49progress ("go test" ) 50before := occupied () 51m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress ,Progress :nil }) 52if got := visible ();strings .Contains (got ,"running python" ) { 53t .Fatalf ("clear progress retained the live action row: %q" ,got ) 54 } 55finish ("run" ,"go test" ) 56if got := visible ();! strings .Contains (got ,"ran go test" )|| strings .Contains (got ,"running python" )|| occupied ()!= before { 57t .Fatalf ("result removed the action row or changed its height: %q, before %d after %d" ,got ,before ,occupied ()) 58 } 59 60progress ("next.go" ) 61if got := visible ();! strings .Contains (got ,"running python" ) { 62t .Fatalf ("the new action did not replace the last: %q" ,got ) 63 } 64finish ("read" ,"next.go" ) 65m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventThinking ,Text :"**Planning the next step**\nmore thought" }) 66thinking := occupied () 67if got := visible ();! strings .Contains (got ,"more thought…" )|| strings .Contains (got ,"Planning the next step" )|| strings .Contains (got ,"running python" ) { 68t .Fatalf ("thinking did not replace the tool action with its newest line: %q" ,got ) 69 } 70m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventUsage }) 71if got := visible ();! strings .Contains (got ,"more thought" )|| strings .Contains (got ,"more thought…" )|| occupied ()!= thinking { 72t .Fatalf ("finished thought removed its newest line: %q, before %d after %d" ,got ,thinking ,occupied ()) 73 } 74m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventText ,Text :"here's why" }) 75if got := visible ();strings .Contains (got ,"more thought" )|| ! strings .Contains (got ,"here's why" ) { 76t .Fatalf ("prose did not replace the thought action: %q" ,got ) 77 } 78} 79 80func TestChatToolResultKeepsOtherInterleavedProgress (t * testing.T ) { 81m := newTestChatModel (t ,& daemon.Session {ID :"s" }) 82callA := & daemon.ToolProgress {CallID :"run:1:0" ,ToolCallID :"native-a" ,Name :"python" ,Phase :"running" } 83callB := & daemon.ToolProgress {CallID :"run:1:1" ,ToolCallID :"native-b" ,Name :"python" ,Phase :"generating" ,Code :& daemon.ToolCodePreview {Text :"print('b')" }} 84m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress ,Progress :callA }) 85m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress ,Progress :callB }) 86m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventTool ,ProgressCallID :callA .CallID ,ToolName :"python" ,ToolResult :"done" }) 87m .SetSize (100 ,30 ) 88m .refreshViewportContent () 89if got := ansi .Strip (m .Viewport .View ());! strings .Contains (got ,"generating python" )|| ! strings .Contains (got ,"print('b')" ) { 90t .Fatalf ("call A's result cleared call B's visible activity: %q" ,got ) 91 } 92} 93 94func TestFinishedActionDoesNotLeakIntoVerboseOrReplayedHistory (t * testing.T ) { 95m := newTestChatModel (t ,& daemon.Session {ID :"s" }) 96m .SetSize (100 ,30 ) 97result := daemon.StreamEvent {Type :daemon .EventTool ,ToolName :"python" , 98ToolArgs :map [string ]any {"code" :`print("tool_source_sentinel")` },ToolResult :`{"status":"ok"}` } 99m .handleStreamEvent (result ) 100m .refreshViewportContent () 101if got := ansi .Strip (m .Viewport .View ());strings .Count (got ,"tool_source_sentinel" )!= 2 { 102t .Fatalf ("compact history and the completed action are not both visible: %q" ,got ) 103 } 104m .TextArea .SetValue ("/verbose" ) 105m ,_ = m .Update (tea.KeyPressMsg {Code :tea .KeyEnter }) 106verboseCount := strings .Count (ansi .Strip (m .Viewport .View ()),"tool_source_sentinel" ) 107if verboseCount == 0 { 108t .Fatal ("verbose history hid the tool source" ) 109 } 110m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventTurnCompleted }) 111m .refreshViewportContent () 112if got := ansi .Strip (m .Viewport .View ());strings .Count (got ,"tool_source_sentinel" )!= verboseCount { 113t .Fatalf ("the completed preview duplicated the verbose tool row: %q" ,got ) 114 } 115m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventReset }) 116m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress ,Replayed :true , 117Progress :& daemon.ToolProgress {CallID :"old" ,Name :"python" ,Phase :"generating" ,Code :& daemon.ToolCodePreview {Text :"obsolete_live_sentinel" }}}) 118result .Replayed = true 119m .handleStreamEvent (result ) 120m .TextArea .SetValue ("/verbose" ) 121m ,_ = m .Update (tea.KeyPressMsg {Code :tea .KeyEnter }) 122if got := ansi .Strip (m .Viewport .View ());strings .Contains (got ,"obsolete_live_sentinel" )|| strings .Count (got ,"tool_source_sentinel" )!= 1 { 123t .Fatalf ("replaying old history produced a live action row: %q" ,got ) 124 } 125} 126 127func TestTurnSignoffReplacesTheLastActionRow (t * testing.T ) { 128m := newTestChatModel (t ,& daemon.Session {ID :"s" }) 129m .SetSize (100 ,30 ) 130m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventUser ,Text :"do it" }) 131m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress , 132Progress :& daemon.ToolProgress {CallID :"progress-1" ,Name :"python" ,Phase :"running" }}) 133m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventTool ,ToolName :"python" , 134ProgressCallID :"progress-1" ,ToolArgs :map [string ]any {"code" :"print(42)" },ToolResult :`{"status":"ok"}` }) 135m .refreshViewportContent () 136view := ansi .Strip (m .Viewport .View ()) 137if strings .Count (view ,"python · print(42)" )!= 1 || ! strings .Contains (view ,"python print(42)" ) { 138t .Fatalf ("group summary and completed action are not both visible: %q" ,view ) 139 } 140status := daemon.AgentStatus {Idle :true } 141m ,_ = m .Update (ChatStatusMsg {SessionID :m .SessionID ,Generation :m .Generation , 142Revision :m .statusRevision ,Status :& status }) 143if strings .Contains (ansi .Strip (m .Viewport .View ()),"python · print(42)" )|| 144! strings .Contains (ansi .Strip (m .Viewport .View ()),"python print(42)" ) { 145t .Fatalf ("turn signoff left an action row behind: %q" ,m .Viewport .View ()) 146 } 147} 148 149func TestMetadataEventsKeepVisibleLiveToolActivity (t * testing.T ) { 150for _ ,kind := range []daemon.EventType {daemon .EventUsage ,daemon .EventCommitted ,daemon .EventTurnMembership ,daemon .EventNote ,daemon .EventCompacted } { 151t .Run (string (kind ),func (t * testing.T ) { 152m := newTestChatModel (t ,& daemon.Session {ID :"s" }) 153m .SetSize (100 ,30 ) 154m .Flags .Tools = true 155m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress ,Progress :& daemon.ToolProgress {CallID :"active" ,Name :"python" ,Phase :"generating" ,Code :& daemon.ToolCodePreview {Text :"live_code_sentinel" }}}) 156m .handleStreamEvent (daemon.StreamEvent {Type :kind }) 157m .refreshViewportContent () 158if got := ansi .Strip (m .Viewport .View ());! strings .Contains (got ,"live_code_sentinel" )|| m .statusLine ()!= "generating call" { 159t .Fatalf ("%s removed visible live activity: %q, status %q" ,kind ,got ,m .statusLine ()) 160 } 161 }) 162 } 163} 164 165func TestIdleSignoffCannotRestoreObsoleteCallAfterNextResult (t * testing.T ) { 166m := newTestChatModel (t ,& daemon.Session {ID :"s" }) 167m .SetSize (100 ,30 ) 168m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress ,Progress :& daemon.ToolProgress {CallID :"old" ,Name :"obsolete_tool" ,Phase :"running" }}) 169status := daemon.AgentStatus {Idle :true } 170m ,_ = m .Update (ChatStatusMsg {SessionID :m .SessionID ,Generation :m .Generation ,Revision :m .statusRevision ,Status :& status }) 171m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress ,Progress :& daemon.ToolProgress {CallID :"fresh" ,Name :"python" ,Phase :"running" }}) 172m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventTool ,ProgressCallID :"fresh" ,ToolName :"python" ,ToolResult :"done" }) 173m .refreshViewportContent () 174if got := ansi .Strip (m .Viewport .View ());strings .Contains (got ,"obsolete_tool" )|| m .statusLine ()== "running obsolete_tool" { 175t .Fatalf ("idle signoff restored obsolete activity: %q" ,got ) 176 } 177} 178 179func TestProseTakesActionSlotWithoutForgettingOtherLiveCalls (t * testing.T ) { 180m := newTestChatModel (t ,& daemon.Session {ID :"s" }) 181m .SetSize (100 ,30 ) 182m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress ,Progress :& daemon.ToolProgress {CallID :"a" ,Name :"still_running" ,Phase :"running" }}) 183m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventText ,Text :"working on it" }) 184m .refreshViewportContent () 185if got := ansi .Strip (m .Viewport .View ());! strings .Contains (got ,"working on it" )|| strings .Contains (got ,"running still_running" ) { 186t .Fatalf ("prose did not take the action slot: %q" ,got ) 187 } 188m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress ,Progress :& daemon.ToolProgress {CallID :"b" ,Name :"python" ,Phase :"running" }}) 189m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventTool ,ProgressCallID :"b" ,ToolName :"python" ,ToolResult :"done" }) 190m .refreshViewportContent () 191if got := ansi .Strip (m .Viewport .View ());! strings .Contains (got ,"running still_running" ) { 192t .Fatalf ("prose forgot call A before call B's result: %q" ,got ) 193 } 194} 195func TestPythonActionShowsCellSourceAndRunningJobCommand (t * testing.T ) { 196progress := & daemon.ToolProgress {CallID :"cell" ,Name :"python" ,Phase :"running" ,Code :& daemon.ToolCodePreview {Text :"# setup\n\nprint(\"hello\")" }} 197if got := actionLabel (progress ,nil );got != `running print("hello")` { 198t .Fatalf ("cell source was not summarized: %q" ,got ) 199 } 200if got := pythonSourceLine (& daemon.ToolCodePreview {Text :"# only comments\n \n" });got != "" { 201t .Fatalf ("comment-only cell should have no summary: %q" ,got ) 202 } 203} 204 205func TestRunningPythonJobCommandAppearsInActionAndFooter (t * testing.T ) { 206m := newTestChatModel (t ,& daemon.Session {ID :"s" }) 207m .SetSize (100 ,30 ) 208m .Status .RunningJobs = []protocol.KernelJob {{ID :"j" ,Command :"./test.sh" }} 209m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventToolProgress , 210Progress :& daemon.ToolProgress {CallID :"cell" ,Name :"python" ,Phase :"running" , 211Code :& daemon.ToolCodePreview {Text :"job = run(\"./test.sh\")\nawait job" }}}) 212m .refreshViewportContent () 213if got := ansi .Strip (m .Viewport .View ());! strings .Contains (got ,"running ./test.sh" ) { 214t .Fatalf ("running command missing from action: %q" ,got ) 215 } 216if got := m .statusLine ();got != "running" { 217t .Fatalf ("the status line repeats the action row: %q" ,got ) 218 } 219}