// These tests catch mutable live-buffer ownership and chunk-boundary
// regressions, and signoffs placed by event order. Daemon E2E cannot retain Go
// model copies, control the exact delta boundaries received by the terminal,
// or see rows. Copies here are retained readers; only the current update path
// appends, settles, or discards live text.
package tui
import (
"slices"
"strings"
"testing"
"unsafe"
"albedo/cli/internal/daemon"
)
func TestChatModelSnapshotsAndHistoryOwnTheirText(t *testing.T) {
model := newTestChatModel(t, &daemon.Session{ID: "snapshot"})
model.SetSize(80, 24)
appendThought := func(text string) {
model, _ = model.Update(ChatStreamEventMsg{
SessionID: model.SessionID, Generation: model.Generation,
Event: daemon.StreamEvent{Type: daemon.EventThinking, Text: text},
})
}
appendThought("old thought")
retained := model
oldThought := retained.renderThought()
appendThought(" continued")
continued := model
appendThought(strings.Repeat(" extended", 1024))
if retained.transcript.activeText != "old thought" || retained.renderThought() != oldThought || continued.transcript.activeText != "old thought continued" {
t.Fatal("append or growth changed a retained snapshot")
}
liveText := model.transcript.activeText
model, _ = model.Update(ChatStreamEventMsg{
SessionID: model.SessionID, Generation: model.Generation,
Event: daemon.StreamEvent{Type: daemon.EventUsage},
})
history := model.History.Entries()
if len(history) != 1 || history[0].Text != liveText {
t.Fatal("settlement lost the live text")
}
// Equal bytes alone cannot detect history retaining the buffer's spare capacity.
if unsafe.StringData(history[0].Text) == unsafe.StringData(liveText) {
t.Fatal("settled history shares live-buffer storage")
}
if model.transcript.activeBuffer != nil || model.transcript.activeText != "" {
t.Fatal("settlement retained the live buffer")
}
appendThought("replacement")
if retained.transcript.activeText != "old thought" || history[0].Text != liveText {
t.Fatal("new stream changed a retained snapshot or settled history")
}
}
func TestTranscriptDiscardPreservesRetainedReaders(t *testing.T) {
for _, eventType := range []daemon.EventType{daemon.EventRetry, daemon.EventReset} {
t.Run(string(eventType), func(t *testing.T) {
state := newTranscriptState()
state.apply(daemon.StreamEvent{Type: daemon.EventText, Text: "discarded"}, "agent")
retained := state
if entries := state.apply(daemon.StreamEvent{Type: eventType}, "agent"); len(entries) != 0 {
t.Fatalf("discard settled %d entries", len(entries))
}
if state.activeBuffer != nil || state.activeText != "" || state.activeKind != StreamKindNone {
t.Fatal("discard retained live buffer ownership")
}
state.apply(daemon.StreamEvent{Type: daemon.EventText, Text: "fresh"}, "agent")
entries := state.apply(daemon.StreamEvent{Type: daemon.EventMessage, Text: "fresh"}, "agent")
if retained.activeText != "discarded" || len(entries) != 1 || entries[0].Text != "fresh" {
t.Fatal("discard changed a retained reader or failed to reset message deduplication")
}
})
}
}
func TestTranscriptSplitsStrictlyAfterAppendingPastLimit(t *testing.T) {
for _, eventType := range []daemon.EventType{daemon.EventText, daemon.EventThinking} {
t.Run(string(eventType), func(t *testing.T) {
state := newTranscriptState()
atLimit := strings.Repeat("a", MaxLiveStreamBytes)
entries := state.apply(daemon.StreamEvent{Type: eventType, Text: atLimit}, "agent")
if len(entries) != 0 || state.activeText != atLimit || state.activeBuffer == nil {
t.Fatal("stream settled at exactly the byte limit")
}
retained := state
entries = state.apply(daemon.StreamEvent{Type: eventType, Text: "b"}, "agent")
if len(entries) != 1 || entries[0].Text != atLimit+"b" || retained.activeText != atLimit {
t.Fatal("crossing split omitted the final delta or changed a retained reader")
}
if state.activeBuffer != nil || state.activeText != "" || state.activeKind != retained.activeKind {
t.Fatal("automatic settlement did not drop the buffer and retain stream kind")
}
oversized := strings.Repeat("c", MaxLiveStreamBytes*2+1)
entries = state.apply(daemon.StreamEvent{Type: eventType, Text: oversized}, "agent")
if len(entries) != 1 || entries[0].Text != oversized || state.activeBuffer != nil || state.activeText != "" {
t.Fatal("oversized delta was split internally or retained a live buffer")
}
})
}
}
func TestTranscriptMessageDeduplicationSpansSettlements(t *testing.T) {
state := newTranscriptState()
first := strings.Repeat("a", MaxLiveStreamBytes+1)
second := "second"
var history []HistoryEntry
history = append(history, state.apply(daemon.StreamEvent{Type: daemon.EventText, Text: first}, "agent")...)
history = append(history, state.apply(daemon.StreamEvent{Type: daemon.EventThinking, Text: "thought"}, "agent")...)
history = append(history, state.apply(daemon.StreamEvent{Type: daemon.EventText, Text: second}, "agent")...)
history = append(history, state.apply(daemon.StreamEvent{Type: daemon.EventUsage}, "agent")...)
if entries := state.apply(daemon.StreamEvent{Type: daemon.EventMessage, Text: first + second}, "agent"); len(entries) != 0 {
t.Fatal("committed message duplicated text settled across multiple boundaries")
}
if len(history) != 3 || history[0].Text != first || history[1].Text != "thought" || history[2].Text != second {
t.Fatal("multiple settlements lost or reordered history")
}
entries := state.apply(daemon.StreamEvent{Type: daemon.EventMessage, Text: first + second}, "agent")
if len(entries) != 1 || entries[0].Text != first+second {
t.Fatal("deduplication did not reset after the committed message")
}
}
func TestANoteTheAgentNeverAnsweredHasNoSignoff(t *testing.T) {
// /link and /work queue a note; it commits right before your next message.
entries := replayTranscript([]daemon.StreamEvent{
{Type: daemon.EventUser, Source: "links", Text: "linked with chernobog:~/proj/albedo", Replayed: true},
{Type: daemon.EventUser, Source: "chat", Text: "hi", Replayed: true},
{Type: daemon.EventMessage, Text: "hello", Replayed: true},
}, "albedo")
var kinds []EntryKind
for _, entry := range entries {
kinds = append(kinds, entry.Kind)
}
if want := []EntryKind{EntryUser, EntryUser, EntryAssistant}; !slices.Equal(kinds, want) {
t.Fatalf("entries %v, want %v", kinds, want)
}
}