okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1// A controlled peer holds snapshots across stream updates and delivers replies 2// late. Real provider timing cannot reliably create these response orderings. 3package tui 4 5import ( 6"albedo/cli/internal/testwire" 7"context" 8"encoding/json" 9"fmt" 10"net" 11"net/http" 12"net/http/httptest" 13"strings" 14"sync/atomic" 15"testing" 16"time" 17 18"albedo/cli/internal/daemon" 19 tea"charm.land/bubbletea/v2" 20"github.com/charmbracelet/x/ansi" 21) 22 23func TestAgentsRefreshDiscardsDirtyAndPreviousSubscriptionReplies (t * testing.T ) { 24ctx ,cancel := context .WithTimeout (t .Context (),10 * time .Second ) 25defer cancel () 26feeds := []chan string {make (chan string ,4 ),make (chan string ,4 ),make (chan string ,4 )} 27failureRelease ,dirtyRelease := make (chan struct {}),make (chan struct {}) 28var streams ,snapshots ,seeds atomic.Int32 29changed := make (chan struct {},16 ) 30notify := func () { 31select { 32case changed <- struct {}{}: 33default : 34 } 35 } 36server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 37switch { 38case r .URL .Path == "/sessions" && r .Header .Get ("Accept" )== "text/event-stream" : 39number := streams .Add (1 ) 40notify () 41if number > int32 (len (feeds )) { 42t .Error ("unexpected extra subscription" ) 43return 44 } 45w .Header ().Set ("Content-Type" ,"text/event-stream" ) 46testwire .InitialStream (w ,"agents" ) 47w .(http.Flusher ).Flush () 48for { 49select { 50case batch := <- feeds [number - 1 ]: 51_ ,_ = fmt .Fprintf (w ,"data: %s\n\n" ,batch ) 52w .(http.Flusher ).Flush () 53if strings .Contains (batch ,"overflow" ) { 54return 55 } 56case <- r .Context ().Done (): 57return 58case <- ctx .Done (): 59return 60 } 61 } 62case r .URL .Path == "/sessions" : 63number := snapshots .Add (1 ) 64notify () 65if streams .Load ()== 0 { 66t .Error ("snapshot preceded subscription readiness" ) 67 } 68name := "LIVE_TREE" 69switch number { 70case 1 : 71select { 72case <- failureRelease : 73case <- ctx .Done (): 74return 75 } 76w .WriteHeader (http .StatusServiceUnavailable ) 77_ ,_ = fmt .Fprint (w ,`{"detail":"snapshot refused"}` ) 78return 79case 2 : 80select { 81case <- dirtyRelease : 82case <- ctx .Done (): 83return 84 } 85name = "DIRTY_TREE" 86case 4 : 87name = "OLD_SUBSCRIPTION_TREE" 88case 5 : 89name = "FINAL_TREE" 90 } 91session := protocolSession ("lead" ,generationA ,0 ) 92session ["name" ]= name 93_ = json .NewEncoder (w ).Encode (map [string ]any {"items" : []any {session },"next" :nil ,"family" :map [string ]any {"root_id" :"lead" ,"revision" :"family-a" }}) 94case r .URL .Path == "/sessions/lead" : 95session := protocolSession ("lead" ,generationA ,0 ) 96if r .URL .Query ().Has ("tail" ) { 97preview := "OLD_SUBSCRIPTION_SEED" 98if seeds .Add (1 )> 1 { 99preview = "FINAL_SEED" 100 } 101session ["preview" ].(map [string ]any )["transcript_count" ]= 1 102session ["history" ].(map [string ]any )["items" ]= []any {protocolEntry ("answer" ,"assistant" ,preview ,1 )} 103 } 104_ = json .NewEncoder (w ).Encode (session ) 105default : 106t .Errorf ("unexpected request: %s" ,r .URL .Path ) 107w .WriteHeader (http .StatusNotFound ) 108 } 109 })) 110defer server .Close () 111defer cancel () 112connection := daemon .NewConnection (daemon.ConnectionSnapshot {Port :server .Listener .Addr ().(* net.TCPAddr ).Port },nil ) 113defer connection .HTTPClient ().CloseIdleConnections () 114model := NewAgentsViewModel (connection ,"lead" ) 115defer func () {model .Close () }() 116model .SetSize (120 ,30 ) 117inbox := make (chan tea.Msg ,16 ) 118var run func (tea.Cmd ) 119run = func (cmd tea.Cmd ) { 120if cmd == nil { 121return 122 } 123go func () { 124message := cmd () 125if batch ,ok := message .(tea.BatchMsg );ok { 126for _ ,child := range batch { 127run (child ) 128 } 129return 130 } 131switch message .(type ) { 132case agentsSnapshotMsg ,agentsEventsMsg ,agentsStreamClosedMsg ,agentsSnapshotRetryMsg ,agentsSeedMsg ,agentsSeedErrMsg : 133select { 134case inbox <- message : 135case <- ctx .Done (): 136 } 137 } 138 }() 139 } 140update := func (message tea.Msg ) {var cmd tea.Cmd ;model ,cmd = model .Update (message );run (cmd ) } 141view := func ()string {return ansi .Strip (model .View ()) } 142pump := func (done func ()bool ) { 143t .Helper () 144for ! done () { 145select { 146case message := <- inbox : 147update (message ) 148case <- changed : 149case <- ctx .Done (): 150t .Fatalf ("refresh did not settle:\n%s" ,view ()) 151 } 152 } 153 } 154take := func (matches func (tea.Msg )bool ) tea.Msg { 155t .Helper () 156for { 157select { 158case message := <- inbox : 159if matches (message ) { 160return message 161 } 162update (message ) 163case <- ctx .Done (): 164t .Fatalf ("expected response never arrived:\n%s" ,view ()) 165return nil 166 } 167 } 168 } 169run (model .Init ()) 170pump (func ()bool {return snapshots .Load ()== 1 }) 171feeds [0 ]<- `{"events":[{"type":"invalidate","data":{"urls":["/sessions"],"session_ids":["lead"],"scope_dirty":true}}]}` 172// The event must be consumed before releasing the failed snapshot. 173takeEvent := take (func (message tea.Msg )bool {batch ,ok := message .(agentsEventsMsg );return ok && len (batch .Events )> 0 }) 174update (takeEvent ) 175close (failureRelease ) 176pump (func ()bool {return strings .Contains (view (),"snapshot refused" ) }) 177pump (func ()bool {return snapshots .Load ()== 2 }) 178feeds [0 ]<- `{"events":[{"type":"invalidate","data":{"urls":["/sessions"],"session_ids":["lead"],"scope_dirty":true}}]}` 179update (take (func (message tea.Msg )bool {batch ,ok := message .(agentsEventsMsg );return ok && len (batch .Events )> 0 })) 180close (dirtyRelease ) 181dirty := take (func (message tea.Msg )bool { 182snapshot ,ok := message .(agentsSnapshotMsg ) 183return ok && snapshot .Err == nil 184 }) 185update (dirty ) 186if strings .Contains (view (),"DIRTY_TREE" ) { 187t .Fatal ("dirty snapshot replaced the live graph" ) 188 } 189pump (func ()bool {return strings .Contains (view (),"LIVE_TREE" ) }) 190oldSeed := take (func (message tea.Msg )bool {_ ,ok := message .(agentsSeedMsg );return ok }) 191feeds [0 ]<- `{"events":[{"type":"overflow","data":{}}]}` 192oldSnapshot := take (func (message tea.Msg )bool { 193snapshot ,ok := message .(agentsSnapshotMsg ) 194return ok && snapshot .Err == nil 195 }) 196feeds [1 ]<- `{"events":[{"type":"overflow","data":{}}]}` 197pump (func ()bool {return strings .Contains (view (),"FINAL_SEED" ) }) 198update (oldSnapshot ) 199update (oldSeed ) 200if strings .Contains (view (),"OLD_SUBSCRIPTION" )|| ! strings .Contains (view (),"FINAL_TREE" )|| ! strings .Contains (view (),"FINAL_SEED" ) { 201t .Fatalf ("late success restored stale membership or history:\n%s" ,view ()) 202 } 203if streams .Load ()!= 3 || snapshots .Load ()!= 5 { 204t .Fatalf ("recovery made unexpected requests: streams=%d snapshots=%d" ,streams .Load (),snapshots .Load ()) 205 } 206}