okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
5b3f8be
main
1//go:build unix 2 3// The agents view must replace stale membership and previews after a lost feed. 4package e2e 5 6import ( 7"context" 8"encoding/json" 9"fmt" 10"net" 11"net/http" 12"net/http/httptest" 13"net/http/httputil" 14"net/url" 15"strings" 16"sync/atomic" 17"testing" 18"time" 19 20"albedo/cli/internal/daemon" 21"albedo/cli/internal/testwire" 22"albedo/cli/internal/tui" 23 tea"charm.land/bubbletea/v2" 24) 25 26func TestTUIAgentsOverflowRefreshesMembershipAndPreviews (t * testing.T ) { 27entered ,release := make (chan struct {}),make (chan struct {}) 28defer close (release ) 29profile := providerRoute (t ,func (request map [string ]any )string { 30if lastUserText (request )== "held agent refresh turn" { 31close (entered ) 32<- release 33 } 34return echoReply (request ) 35 }) 36t .Parallel () 37root := daemonSession (t ,newSession (t ,t .TempDir ())) 38create := func (name string ) daemon.Session { 39t .Helper () 40child ,err := daemon .CreateSession (t .Context (),conn (t ), daemon.CreateSessionRequest {Kind :"child" ,ParentID :root .ID ,Address :name ,Name :name ,Task :"finish child setup" }) 41if err != nil { 42t .Fatal (err ) 43 } 44waitIdle (t ,child .ID ,profile ,1 ) 45return child 46 } 47removed := create ("removed" ) 48retained := create ("old-name" ) 49waitIdle (t ,root .ID ,profile ,2 ) 50if _ ,err := daemon .NewChatClient (conn (t ),root .ID ).Send (t .Context (),"INITIAL_PREVIEW_SEED" ,nil );err != nil { 51t .Fatal (err ) 52 } 53waitIdle (t ,root .ID ,profile ,3 ) 54 55destination ,err := url .Parse (conn (t ).BaseURL ()) 56if err != nil { 57t .Fatal (err ) 58 } 59forward := httputil .NewSingleHostReverseProxy (destination ) 60direct := forward .Director 61forward .Director = func (r * http.Request ) {direct (r );r .Host = destination .Host } 62forward .FlushInterval = - 1 63var streams atomic.Int32 64seedReady := make (chan struct {},1 ) 65fragments ,gap := make (chan struct {}),make (chan struct {}) 66server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 67if r .URL .Path == "/sessions" && r .Header .Get ("Accept" )== "text/event-stream" && streams .Add (1 )== 1 { 68w .Header ().Set ("Content-Type" ,"text/event-stream" ) 69_ ,_ = fmt .Fprint (w ,"data: {\"events\":[{\"type\":\"ready\",\"data\":{\"instance_id\":\"controlled-agent-stream\"}},{\"type\":\"reset\",\"data\":{\"reason\":\"initial\"}}]}\n\n" ) 70w .(http.Flusher ).Flush () 71select { 72case <- fragments : 73case <- r .Context ().Done (): 74return 75 } 76events ,_ := json .Marshal (map [string ]any {"events" : []map [string ]any { 77 {"type" :"activity" ,"data" :map [string ]any {"session_id" :root .ID ,"cursor" :map [string ]any {"generation" :testwire .GenerationA ,"sequence" :1 },"status" :map [string ]any {"phase" :"running" ,"run_id" :"old-run" ,"interrupt_requested" :false ,"blocking_reason" :nil },"current_progress" : []map [string ]any {{"call_id" :"old-run:1:0" ,"tool_call_id" :nil ,"name" :"python" ,"phase" :"generating" ,"intent" :"" ,"preview" :map [string ]any {"offset_scalars" :0 ,"text" :"OLD_PARTIAL_CODE" ,"complete" :false }}},"activity" :map [string ]any {"lines" : []map [string ]any {{"kind" :"assistant" ,"text" :"OLD_PARTIAL_TEXT" ,"truncated" :false }},"output_scalars" :0 ,"output_utf8_bytes" :0 ,"observed_at" :"2026-10-03T00:00:00Z" ,"latest_input" :nil ,"latest_answer" :nil }}}, 78 }}) 79_ ,_ = fmt .Fprintf (w ,"data: %s\n\n" ,events ) 80w .(http.Flusher ).Flush () 81select { 82case <- gap : 83_ ,_ = fmt .Fprint (w ,"data: {\"events\":[{\"type\":\"overflow\",\"data\":{\"reason\":\"subscriber_overflow\"}}]}\n\n" ) 84w .(http.Flusher ).Flush () 85case <- r .Context ().Done (): 86 } 87return 88 } 89forward .ServeHTTP (w ,r ) 90if r .URL .Path == "/sessions/" + root .ID && r .URL .Query ().Get ("tail" )!= "" { 91select { 92case seedReady <- struct {}{}: 93default : 94 } 95 } 96 })) 97t .Cleanup (server .Close ) 98snapshot := conn (t ).Snapshot () 99snapshot .Port = server .Listener .Addr ().(* net.TCPAddr ).Port 100connection ,err := daemon .Attach (t .Context (),snapshot ,nil ) 101if err != nil { 102t .Fatal (err ) 103 } 104t .Cleanup (connection .HTTPClient ().CloseIdleConnections ) 105driver := driveTUIWithConnection (t ,& root ,connection ) 106driver .Update (tea.WindowSizeMsg {Width :140 ,Height :50 }) 107t .Cleanup (driver .App .Chat .Close ) 108t .Cleanup (func () {driver .App .Agents .Close () }) 109ctx ,cancel := context .WithTimeout (t .Context (),20 * time .Second ) 110defer cancel () 111inbox := make (chan tea.Msg ,64 ) 112streamMessages (ctx ,driver .Update (tui.ChatOpenAgentsMsg {}),inbox ) 113pump := func (condition func ()bool ) { 114t .Helper () 115for ! condition () { 116select { 117case message := <- inbox : 118streamMessages (ctx ,driver .Update (message ),inbox ) 119case <- ctx .Done (): 120t .Fatalf ("agent view did not recover:\n%s" ,driver .View ()) 121 } 122 } 123 } 124pump (func ()bool {return strings .Contains (driver .View (),"INITIAL_PREVIEW_SEED" ) }) 125select { 126case <- seedReady : 127case <- ctx .Done (): 128t .Fatal ("initial preview request never completed" ) 129 } 130close (fragments ) 131pump (func ()bool {return strings .Contains (driver .View (),"OLD_PARTIAL_CODE" ) }) 132if ! strings .Contains (driver .View (),"removed" )|| ! strings .Contains (driver .View (),"old-name" ) { 133t .Fatalf ("initial membership did not load:\n%s" ,driver .View ()) 134 } 135if _ ,err := daemon .RenameSession (t .Context (),conn (t ),retained .ID ,"renamed" ,sessionCondition (t ,conn (t ),retained .ID ).ETag );err != nil { 136t .Fatal (err ) 137 } 138if _ ,err := daemon .DeleteSession (t .Context (),conn (t ),removed .ID ,false ,sessionCondition (t ,conn (t ),removed .ID ));err != nil { 139t .Fatal (err ) 140 } 141create ("new-agent" ) 142if _ ,err := daemon .NewChatClient (conn (t ),root .ID ).Send (t .Context (),"held agent refresh turn" ,nil );err != nil { 143t .Fatal (err ) 144 } 145select { 146case <- entered : 147case <- ctx .Done (): 148t .Fatal ("held model turn did not start" ) 149 } 150for _ ,character := range "draft survives refresh" { 151driver .Update (tea.KeyPressMsg {Code :character ,Text :string (character )}) 152 } 153close (gap ) 154pump (func ()bool { 155view := driver .View () 156return strings .Contains (view ,"renamed" )&& strings .Contains (view ,"new-agent" )&& strings .Contains (view ,"1 running" )&& ! strings .Contains (view ,"○ removed" )&& ! strings .Contains (view ,"○ old-name" ) 157 }) 158view := driver .View () 159if strings .Contains (view ,"OLD_PARTIAL" )|| ! strings .Contains (view ,"draft survives refresh" ) { 160t .Fatalf ("refresh retained stale fragments or discarded the input draft:\n%s" ,view ) 161 } 162}