okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
b0282b1
main
1//go:build unix 2 3// A TUI opened mid-response must hydrate the prefix before consuming its suffix, 4// and replace provisional rows with exactly one canonical committed response. 5package e2e 6 7import ( 8"context" 9"encoding/json" 10"fmt" 11"net/http" 12"net/http/httptest" 13"strings" 14"sync" 15"testing" 16"time" 17 18"albedo/cli/internal/config" 19"albedo/cli/internal/daemon" 20"albedo/cli/internal/tui" 21 tea"charm.land/bubbletea/v2" 22) 23 24func TestTUIActiveOutputAttach (t * testing.T ) { 25for _ ,size := range []int {32 ,90000 } { 26t .Run (fmt .Sprint (size ),func (t * testing.T ) { 27prefix := strings .Repeat ("p" ,size )+ " paused α🙂" 28thinking := "thinking β" 29suffix := " final suffix" 30release := make (chan struct {}) 31var releaseOnce sync.Once 32resume := func () {releaseOnce .Do (func () {close (release ) }) } 33t .Cleanup (resume ) 34server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) { 35w .Header ().Set ("Content-Type" ,"text/event-stream" ) 36flush := w .(http.Flusher ) 37emit := func (field ,text string ) { 38payload ,_ := json .Marshal (map [string ]any {"id" :"fixture" ,"choices" : []any {map [string ]any { 39"index" :0 ,"delta" :map [string ]string {field :text },"finish_reason" :nil , 40 }}}) 41fmt .Fprintf (w ,"data: %s\n\n" ,payload ) 42flush .Flush () 43 } 44emit ("reasoning_content" ,thinking ) 45for _ ,part := range chunks (prefix ,1024 ) { 46emit ("content" ,part ) 47 } 48select { 49case <- release : 50case <- r .Context ().Done (): 51return 52 } 53emit ("content" ,suffix ) 54fmt .Fprint (w ,"data: {\"id\":\"fixture\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n" ) 55flush .Flush () 56 })) 57t .Cleanup (server .Close ) 58profile := fmt .Sprintf ("active-output-%d" ,size ) 59if err := saveAndSelectProvider (t .Context (),conn (t ),profile , config.Settings { 60Extension :"openai" ,BaseURL :server .URL ,APIKey :"fixture" ,Model :"fixture-model" ,Protocol :"chat_completions" , 61 });err != nil { 62t .Fatal (err ) 63 } 64created ,err := daemon .CreateSession (t .Context (),conn (t ), daemon.CreateSessionRequest {Workspace :t .TempDir (),Provider :profile }) 65if err != nil { 66t .Fatal (err ) 67 } 68session := daemonSession (t ,created .ID ) 69ctx ,cancel := context .WithTimeout (t .Context (),30 * time .Second ) 70defer cancel () 71observed := make (chan struct {}) 72ready := make (chan struct {}) 73var readyOnce sync.Once 74observerErrors := make (chan error ,1 ) 75go func () { 76var text strings.Builder 77observerErrors <- daemon .NewChatClient (conn (t ),session .ID ).Stream (ctx ,0 ,func (event daemon.StreamEvent )error { 78readyOnce .Do (func () {close (ready ) }) 79if event .Type == daemon .EventText { 80text .WriteString (event .Text ) 81if text .String ()== prefix { 82close (observed ) 83 } 84 } 85return nil 86 }) 87 }() 88select { 89case <- ready : 90case err := <- observerErrors : 91t .Fatalf ("responsive subscriber did not attach: %v" ,err ) 92case <- ctx .Done (): 93t .Fatal ("responsive subscriber did not attach" ) 94 } 95if _ ,err := daemon .NewChatClient (conn (t ),session .ID ).Send (ctx ,"fixture" ,nil );err != nil { 96t .Fatal (err ) 97 } 98select { 99case <- observed : 100case err := <- observerErrors : 101t .Fatalf ("responsive subscriber ended: %v" ,err ) 102case <- ctx .Done (): 103t .Fatal ("responsive subscriber did not see complete prefix" ) 104 } 105driver := driveTUI (t ,& session ) 106t .Cleanup (driver .App .Chat .Close ) 107inbox := make (chan tea.Msg ,32 ) 108attachedAt := time .Now () 109streamMessages (ctx ,driver .App .Chat .Init (),inbox ) 110var restored ,restoredThinking strings.Builder 111resumed := false 112committed := false 113for { 114select { 115case msg := <- inbox : 116cmd := driver .Update (msg ) 117if event ,ok := msg .(tui.ChatStreamEventMsg );ok { 118switch event .Event .Type { 119case daemon .EventCommitted : 120committed = true 121case daemon .EventText : 122if ! resumed { 123restored .WriteString (event .Event .Text ) 124 } 125case daemon .EventThinking : 126if ! resumed { 127restoredThinking .WriteString (event .Event .Text ) 128 } 129 } 130 } 131if ! resumed && restored .String ()== prefix && restoredThinking .String ()== thinking { 132resumed = true 133t .Logf ("Init to hydrated TUI prefix (%d bytes text): %s" ,len (prefix ),time .Since (attachedAt )) 134resume () 135 } 136assistantCount := 0 137for _ ,entry := range driver .App .Chat .History .Entries () { 138if entry .Kind == tui .EntryAssistant && entry .Text == prefix + suffix && entry .Seq > 0 { 139assistantCount ++ 140 } 141 } 142if committed && assistantCount > 0 { 143if assistantCount != 1 { 144t .Fatalf ("duplicate canonical responses: %d" ,assistantCount ) 145 } 146for _ ,entry := range driver .App .Chat .History .Entries () { 147if entry .Kind == tui .EntryAssistant && entry .Seq == 0 { 148t .Fatalf ("provisional response survived canonical commit: %+v" ,entry ) 149 } 150 } 151return 152 } 153streamMessages (ctx ,cmd ,inbox ) 154case <- ctx .Done (): 155t .Fatalf ("attachment lost prefix or duplicated response: restored=%d want=%d thinking=%q resumed=%v\n%s" ,restored .Len (),len (prefix ),restoredThinking .String (),resumed ,driver .View ()) 156 } 157 } 158 }) 159 } 160}