okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
0a3eddd
main
1// These tests catch mutable live-buffer ownership and chunk-boundary 2// regressions, and signoffs placed by event order. Daemon E2E cannot retain Go 3// model copies, control the exact delta boundaries received by the terminal, 4// or see rows. Copies here are retained readers; only the current update path 5// appends, settles, or discards live text. 6package tui 7 8import ( 9"slices" 10"strings" 11"testing" 12"unsafe" 13 14"albedo/cli/internal/daemon" 15) 16 17func TestObservedZeroThinkingDurationStopsLocalClock (t * testing.T ) { 18state := newTranscriptState () 19state .apply (daemon.StreamEvent {Type :daemon .EventThinking ,Text :"thought" },"agent" ) 20if state .thinkingSince .IsZero () { 21t .Fatal ("live thought did not start its local clock" ) 22 } 23state .apply (daemon.StreamEvent {Type :daemon .EventThinking ,ElapsedObserved :true ,ElapsedMs :0 },"agent" ) 24if ! state .thinkingSince .IsZero ()|| state .thoughtMs != 0 { 25t .Fatal ("observed zero duration was treated as an unknown duration" ) 26 } 27} 28 29// Completion is a separate canonical observation; the server sends no 30// synthetic interrupted event. It must settle the live tail and close the 31// turn once, including its stopped/failed footer. 32func TestCanonicalCompletionSettlesAndClosesLiveTurn (t * testing.T ) { 33for _ ,source := range []string {"" ,"interrupted" ,"failed" } { 34t .Run (source ,func (t * testing.T ) { 35state := newTranscriptState () 36state .apply (daemon.StreamEvent {Type :daemon .EventUser ,Text :"question" ,Source :"chat" },"agent" ) 37state .apply (daemon.StreamEvent {Type :daemon .EventText ,Text :"answer" ,MessageID :"live-answer" },"agent" ) 38entries := state .apply (daemon.StreamEvent {Type :daemon .EventTurnCompleted ,Source :source },"agent" ) 39if state .turn != nil || state .activeText != "" || len (entries )!= 2 || entries [0 ].Kind != EntryAssistant || entries [0 ].Text != "answer" || entries [1 ].Kind != EntryTurnEnd { 40t .Fatalf ("completion did not settle and close the turn: %#v" ,entries ) 41 } 42if source == "interrupted" && entries [1 ].Mood != moodStopped || source == "failed" && entries [1 ].Mood != moodFailed { 43t .Fatalf ("completion lost its terminal outcome: %#v" ,entries [1 ]) 44 } 45if repeated := state .apply (daemon.StreamEvent {Type :daemon .EventTurnCompleted ,Source :source },"agent" );len (repeated )!= 0 { 46t .Fatalf ("repeated completion created a duplicate footer: %#v" ,repeated ) 47 } 48 }) 49 } 50} 51 52func TestChatModelSnapshotsAndHistoryOwnTheirText (t * testing.T ) { 53model := newTestChatModel (t ,& daemon.Session {ID :"snapshot" }) 54model .SetSize (80 ,24 ) 55appendThought := func (text string ) { 56model ,_ = model .Update (ChatStreamEventMsg { 57SessionID :model .SessionID ,Generation :model .Generation , 58Event : daemon.StreamEvent {Type :daemon .EventThinking ,Text :text }, 59 }) 60 } 61appendThought ("old thought" ) 62retained := model 63oldThought := retained .renderThought () 64appendThought (" continued" ) 65continued := model 66appendThought (strings .Repeat (" extended" ,1024 )) 67if retained .transcript .activeText != "old thought" || retained .renderThought ()!= oldThought || continued .transcript .activeText != "old thought continued" { 68t .Fatal ("append or growth changed a retained snapshot" ) 69 } 70liveText := model .transcript .activeText 71model ,_ = model .Update (ChatStreamEventMsg { 72SessionID :model .SessionID ,Generation :model .Generation , 73Event : daemon.StreamEvent {Type :daemon .EventUsage }, 74 }) 75history := model .History .Entries () 76if len (history )!= 1 || history [0 ].Text != liveText { 77t .Fatal ("settlement lost the live text" ) 78 } 79// Equal bytes alone cannot detect history retaining the buffer's spare capacity. 80if unsafe .StringData (history [0 ].Text )== unsafe .StringData (liveText ) { 81t .Fatal ("settled history shares live-buffer storage" ) 82 } 83if model .transcript .activeBuffer != nil || model .transcript .activeText != "" { 84t .Fatal ("settlement retained the live buffer" ) 85 } 86appendThought ("replacement" ) 87if retained .transcript .activeText != "old thought" || history [0 ].Text != liveText { 88t .Fatal ("new stream changed a retained snapshot or settled history" ) 89 } 90} 91 92func TestTranscriptDiscardPreservesRetainedReaders (t * testing.T ) { 93for _ ,eventType := range []daemon.EventType {daemon .EventRetry ,daemon .EventReset } { 94t .Run (string (eventType ),func (t * testing.T ) { 95state := newTranscriptState () 96state .apply (daemon.StreamEvent {Type :daemon .EventText ,Text :"discarded" },"agent" ) 97retained := state 98if entries := state .apply (daemon.StreamEvent {Type :eventType },"agent" );len (entries )!= 0 { 99t .Fatalf ("discard settled %d entries" ,len (entries )) 100 } 101if state .activeBuffer != nil || state .activeText != "" || state .activeKind != StreamKindNone { 102t .Fatal ("discard retained live buffer ownership" ) 103 } 104state .apply (daemon.StreamEvent {Type :daemon .EventText ,Text :"fresh" ,MessageID :"fresh-live" },"agent" ) 105state .apply (daemon.StreamEvent {Type :daemon .EventMessage ,Text :"fresh" ,EntryID :"fresh-stored" ,Position :1 },"agent" ) 106entries := state .apply (daemon.StreamEvent {Type :daemon .EventCommitted ,Seq :1 ,ReplacesLiveIDs : []string {"fresh-live" }},"agent" ) 107if retained .activeText != "discarded" || len (entries )!= 1 || entries [0 ].Text != "fresh" { 108t .Fatal ("discard changed a retained reader or failed to reset message deduplication" ) 109 } 110 }) 111 } 112} 113 114func TestTranscriptSplitsStrictlyAfterAppendingPastLimit (t * testing.T ) { 115for _ ,eventType := range []daemon.EventType {daemon .EventText ,daemon .EventThinking } { 116t .Run (string (eventType ),func (t * testing.T ) { 117state := newTranscriptState () 118atLimit := strings .Repeat ("a" ,MaxLiveStreamBytes ) 119entries := state .apply (daemon.StreamEvent {Type :eventType ,Text :atLimit },"agent" ) 120if len (entries )!= 0 || state .activeText != atLimit || state .activeBuffer == nil { 121t .Fatal ("stream settled at exactly the byte limit" ) 122 } 123retained := state 124entries = state .apply (daemon.StreamEvent {Type :eventType ,Text :"b" },"agent" ) 125if len (entries )!= 1 || entries [0 ].Text != atLimit + "b" || retained .activeText != atLimit { 126t .Fatal ("crossing split omitted the final delta or changed a retained reader" ) 127 } 128if state .activeBuffer != nil || state .activeText != "" || state .activeKind != retained .activeKind { 129t .Fatal ("automatic settlement did not drop the buffer and retain stream kind" ) 130 } 131oversized := strings .Repeat ("c" ,MaxLiveStreamBytes * 2 + 1 ) 132entries = state .apply (daemon.StreamEvent {Type :eventType ,Text :oversized },"agent" ) 133if len (entries )!= 1 || entries [0 ].Text != oversized || state .activeBuffer != nil || state .activeText != "" { 134t .Fatal ("oversized delta was split internally or retained a live buffer" ) 135 } 136 }) 137 } 138} 139 140func TestCommittedIdentityReplacesAllProvisionalChunks (t * testing.T ) { 141model := newTestChatModel (t ,& daemon.Session {ID :"s" }) 142first := strings .Repeat ("a" ,MaxLiveStreamBytes + 1 ) 143model .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventText ,Text :first ,MessageID :"live-text" }) 144model .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventThinking ,Text :"thought" ,MessageID :"live-thought" }) 145model .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventText ,Text :"second" ,MessageID :"live-text" }) 146model .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventUsage }) 147model .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventMessage ,Text :first + "second" ,EntryID :"stored-text" ,Position :7 }) 148model .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventThinking ,Text :"thought" ,EntryID :"stored-thought" ,Position :8 }) 149model .handleStreamEvent (daemon.StreamEvent {Type :daemon .EventCommitted ,Seq :8 ,ReplacesAllLive :true ,ReplacesLiveIDs : []string {}}) 150entries := model .History .Entries () 151if len (entries )!= 2 || entries [0 ].ID != "stored-text" || entries [0 ].Text != first + "second" || entries [1 ].ID != "stored-thought" || entries [1 ].MessageID != "" { 152t .Fatalf ("commit retained provisional chunks or lost canonical content: %#v" ,entries ) 153 } 154} 155 156// A turn's rate is its latest timed call's: a usage record that arrives again 157// changes nothing, and a call the daemon did not time leaves the rate alone. 158func TestTurnRateIsItsLatestTimedCall (t * testing.T ) { 159rate := func (tokensPerSecond float64 )* daemon.Usage { 160return & daemon.Usage {TokensPerSecond :& tokensPerSecond } 161 } 162state := newTranscriptState () 163state .apply (daemon.StreamEvent {Type :daemon .EventUser ,Source :"chat" ,Text :"hi" },"agent" ) 164state .apply (daemon.StreamEvent {Type :daemon .EventText ,Text :"answer" },"agent" ) 165state .apply (daemon.StreamEvent {Type :daemon .EventUsage ,Usage :rate (40 )},"agent" ) 166state .apply (daemon.StreamEvent {Type :daemon .EventUsage ,Usage :rate (84.2 )},"agent" ) 167state .apply (daemon.StreamEvent {Type :daemon .EventUsage ,Usage :rate (84.2 )},"agent" ) 168state .apply (daemon.StreamEvent {Type :daemon .EventUsage ,Usage :& daemon.Usage {}},"agent" ) 169entries := state .apply (daemon.StreamEvent {Type :daemon .EventTurnCompleted },"agent" ) 170if len (entries )!= 1 || entries [0 ].Kind != EntryTurnEnd || entries [0 ].TokensPerSecond != 84.2 { 171t .Fatalf ("turn ended with entries %+v, want a signoff at 84.2 tok/s" ,entries ) 172 } 173} 174 175func TestANoteTheAgentNeverAnsweredHasNoSignoff (t * testing.T ) { 176// /link and /work queue a note; it commits right before your next message. 177entries := replayTranscript ([]daemon.StreamEvent { 178 {Type :daemon .EventUser ,Source :"links" ,Text :"linked with chernobog:~/proj/albedo" ,Replayed :true }, 179 {Type :daemon .EventUser ,Source :"chat" ,Text :"hi" ,Replayed :true }, 180 {Type :daemon .EventMessage ,Text :"hello" ,Replayed :true }, 181 },"albedo" ) 182var kinds []EntryKind 183for _ ,entry := range entries { 184kinds = append (kinds ,entry .Kind ) 185 } 186if want := []EntryKind {EntryUser ,EntryUser ,EntryAssistant };! slices .Equal (kinds ,want ) { 187t .Fatalf ("entries %v, want %v" ,kinds ,want ) 188 } 189} 190 191func TestTranscriptDeduplicatedMessageKeepsCommittedIdentity (t * testing.T ) { 192state := newTranscriptState () 193state .apply (daemon.StreamEvent {Type :daemon .EventText ,Text :"answer" ,MessageID :"live-answer" },"agent" ) 194state .apply (daemon.StreamEvent { 195Type :daemon .EventMessage ,Text :"answer" ,EntryID :"message-1" ,Position :7 , 196 },"agent" ) 197entries := state .apply (daemon.StreamEvent {Type :daemon .EventCommitted ,Seq :7 ,ReplacesLiveIDs : []string {"live-answer" }},"agent" ) 198if len (entries )!= 1 || entries [0 ].ID != "message-1" || entries [0 ].Seq != 7 { 199t .Fatalf ("settled message lost committed identity: %#v" ,entries ) 200 } 201}