okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
2ce1ecd
main
1// A live tool burst must settle once and invalidate its cache on reset and eviction. 2package tui 3 4import ( 5"fmt" 6"strings" 7"testing" 8 9"albedo/cli/internal/daemon" 10"github.com/charmbracelet/x/ansi" 11) 12 13func traced (activities []daemon.ToolActivity ,changes ... daemon.FileChange )HistoryEntry { 14return HistoryEntry {Kind :EntryTool ,ToolName :"python" ,ToolResult :`{"status":"ok"}` , 15ToolTrace :& daemon.ToolTrace {Activities :activities ,Changes :changes }} 16} 17 18func burstFixture () []HistoryEntry { 19ws := "/w/albedo/" 20return []HistoryEntry { 21 {Kind :EntryThinking ,Text :"hm" ,ElapsedMs :34_000 }, 22traced ([]daemon.ToolActivity {{Kind :"read" ,Target :ws + "cli/internal/tui/chat.go" }, {Kind :"read" ,Target :ws + "cli/internal/tui/transcript.go" }}), 23traced ([]daemon.ToolActivity {{Kind :"run" ,Target :"cd cli && go test ./..." }}), 24traced ([]daemon.ToolActivity {{Kind :"run" ,Target :"go test ./internal/tui" }}), 25traced ([]daemon.ToolActivity {{Kind :"read" ,Target :ws + "priv/python/albedo_trace.py" }}, 26 daemon.FileChange {Path :ws + "priv/python/albedo_trace.py" ,Kind :"diff" ,Added :5 ,Removed :2 }), 27 {Kind :EntryTool ,ToolName :"python" ,ToolResult :`{"status":"error","error":"boom"}` ,ToolArgs :map [string ]any {"code" :"1/0" }}, 28 } 29} 30 31func TestBurstSettlesOnceProseEndsIt (t * testing.T ) { 32m := newTestChatModel (t ,& daemon.Session {ID :"s" ,Workspace :"/w/albedo" }) 33m .SetSize (120 ,40 ) 34m .appendSettledEntry (HistoryEntry {Kind :EntryUser ,Text :"go" }) 35settled := len (m .settledLines ) 36for _ ,entry := range burstFixture ()[:4 ] { 37m .appendSettledEntry (entry ) 38 } 39if len (m .settledLines )!= settled { 40t .Fatal ("an open burst wrote settled rows" ) 41 } 42m .refreshViewportContent () 43if view := ansi .Strip (m .Viewport .View ());! strings .Contains (view ,"ran go test ×2" ) { 44t .Fatalf ("the open burst is not drawn: %q" ,view ) 45 } 46m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventMessage ,Text :"done" }) 47joined := ansi .Strip (strings .Join (m .settledLines [settled :],"\n" )) 48if strings .Count (joined ,"ran go test" )!= 1 || ! strings .Contains (joined ,"done" )|| 49strings .Index (joined ,"ran go test" )> strings .Index (joined ,"done" ) { 50t .Fatalf ("burst did not settle once, before the prose: %q" ,joined ) 51 } 52m .rebuildSettledLines () 53if again := ansi .Strip (strings .Join (m .settledLines ,"\n" ));strings .Count (again ,"ran go test" )!= 1 { 54t .Fatalf ("a rebuild drew the burst %d times" ,strings .Count (again ,"ran go test" )) 55 } 56} 57 58// The cached live frame renders exactly what collecting the same entries 59// renders, and is rebuilt only when one of the inputs in its key changes: 60// each step below mutates exactly one, and the render counter must move once. 61func TestCachedBurstMatchesCollectedBurst (t * testing.T ) { 62m := newTestChatModel (t ,& daemon.Session {ID :"s" ,Workspace :"/w/albedo" }) 63m .SetSize (120 ,30 ) 64for _ ,entry := range burstFixture () { 65m .appendSettledEntry (entry ) 66 } 67m .refreshViewportContent () 68entries := m .History .Entries () 69burst := trailingBurst (entries ,m .Flags ) 70if len (burst )!= len (burstFixture ()) { 71t .Fatalf ("trailing burst is %d entries, want %d" ,len (burst ),len (burstFixture ())) 72 } 73want := strings .Join (m .Renderer .BurstBlock (entries [:len (entries )- len (burst )],burst ,m .Flags ),"\n" ) 74if got := strings .Join (m .burstRows ,"\n" );got != want { 75t .Fatalf ("cached frame diverged:\n%s\nwant:\n%s" ,got ,want ) 76 } 77 78renders := m .burstRenders 79step := func (name string ,mutate func (),want int ) { 80mutate () 81m .refreshViewportContent () 82if got := m .burstRenders - renders ;got != want { 83t .Fatalf ("%s: frame rebuilt %d times, want %d" ,name ,got ,want ) 84 } 85renders = m .burstRenders 86 } 87step ("idle refresh" ,func () {},0 ) 88step ("a settled entry" ,func () {m .appendSettledEntry (burstFixture ()[0 ]) },1 ) 89step ("width" ,func () {m .SetSize (100 ,30 ) },1 ) 90// the frame depends on the flags (diffs render the changes), so each flip 91// redraws once; flipping back lands on the cached frame again 92step ("flags there and back" ,func () {m .Flags .Tools = true ;m .refreshViewportContent ();m .Flags .Tools = false },2 ) 93step ("workspace" ,func () {m .Renderer .Workspace = "/w/other" },1 ) 94step ("reset then the same entries" ,func () { 95m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventReset }) 96for _ ,entry := range burstFixture () { 97m .appendSettledEntry (entry ) 98 } 99 },1 ) 100// same entry count after a reset, different content: only the epoch tells 101// the cache these are not the entries it rendered 102other := burstFixture () 103other [4 ].ToolTrace .Changes [0 ].Path = "/w/albedo/cli/internal/tui/other.go" 104step ("reset then same-sized different entries" ,func () { 105m .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventReset }) 106for _ ,entry := range other { 107m .appendSettledEntry (entry ) 108 } 109 },1 ) 110m .SetSize (240 ,30 )// wide enough that the ran list is not counted away 111m .refreshViewportContent () 112entries = m .History .Entries () 113burst = trailingBurst (entries ,m .Flags ) 114want = strings .Join (m .Renderer .BurstBlock (entries [:len (entries )- len (burst )],burst ,m .Flags ),"\n" ) 115if got := strings .Join (m .burstRows ,"\n" );got != want || ! strings .Contains (got ,"tui/other.go" ) { 116t .Fatalf ("same-sized history after a reset kept the old frame:\n%s\nwant:\n%s" ,got ,want ) 117 } 118// the same equivalence under the width and workspace the steps left behind 119entries = m .History .Entries () 120burst = trailingBurst (entries ,m .Flags ) 121want = strings .Join (m .Renderer .BurstBlock (entries [:len (entries )- len (burst )],burst ,m .Flags ),"\n" ) 122if strings .Join (m .burstRows ,"\n" )!= want { 123t .Fatalf ("after a reset the frame went stale:\n%s\nwant:\n%s" ,strings .Join (m .burstRows ,"\n" ),want ) 124 } 125} 126 127// Eviction swaps the head of history without changing its length, so the 128// cached frame must key on evictions too or the live burst freezes. 129func TestEvictionKeepsTheLiveBurstCurrent (t * testing.T ) { 130m := newTestChatModel (t ,& daemon.Session {ID :"s" ,Workspace :"/w" }) 131m .SetSize (120 ,30 ) 132m .History = NewBoundedHistory (4 ,1 << 20 ) 133for i := range 8 { 134m .appendSettledEntry (traced ([]daemon.ToolActivity {{Kind :"read" ,Target :fmt .Sprintf ("/w/f%d.go" ,i )}})) 135m .refreshViewportContent () 136 } 137rows := strings .Join (m .burstRows ,"\n" ) 138if ! strings .Contains (rows ,"f7}" )|| strings .Contains (rows ,"f0" )|| strings .Contains (rows ,"f3" ) { 139t .Fatalf ("live burst froze across eviction: %q" ,rows ) 140 } 141}