@@ -109,6 +109,160 @@ func TestBuildMessagesHappyPath(t *testing.T) {
109109 }
110110}
111111
112+ func TestReduceReturnsMessagesOnReduceError (t * testing.T ) {
113+ events := trackEventsFrom (
114+ aguievents .NewTextMessageStartEvent ("user-1" , aguievents .WithRole ("user" )),
115+ aguievents .NewTextMessageContentEvent ("user-1" , "hello" ),
116+ aguievents .NewTextMessageEndEvent ("user-1" ),
117+ aguievents .NewTextMessageContentEvent ("user-1" , "!" ),
118+ )
119+ msgs , err := Reduce (testAppName , testUserID , events )
120+ if err == nil || ! strings .Contains (err .Error (), "reduce: text message content after end: user-1" ) {
121+ t .Fatalf ("unexpected error %v" , err )
122+ }
123+ if len (msgs ) != 1 {
124+ t .Fatalf ("expected 1 message, got %d" , len (msgs ))
125+ }
126+ if msgs [0 ].Content == nil || * msgs [0 ].Content != "hello" {
127+ t .Fatalf ("unexpected content %v" , msgs [0 ].Content )
128+ }
129+ }
130+
131+ func TestReduceReturnsMessagesOnFinalizeError (t * testing.T ) {
132+ events := trackEventsFrom (
133+ aguievents .NewTextMessageStartEvent ("user-1" , aguievents .WithRole ("user" )),
134+ aguievents .NewTextMessageContentEvent ("user-1" , "hello" ),
135+ )
136+ msgs , err := Reduce (testAppName , testUserID , events )
137+ if err == nil || ! strings .Contains (err .Error (), "finalize: text message user-1 not closed" ) {
138+ t .Fatalf ("unexpected error %v" , err )
139+ }
140+ if len (msgs ) != 1 {
141+ t .Fatalf ("expected 1 message, got %d" , len (msgs ))
142+ }
143+ if msgs [0 ].Content != nil {
144+ t .Fatalf ("expected nil content, got %v" , msgs [0 ].Content )
145+ }
146+ }
147+
148+ func TestHandleTextChunkSuccess (t * testing.T ) {
149+ tests := []struct {
150+ name string
151+ chunk * aguievents.TextMessageChunkEvent
152+ wantRole string
153+ wantName string
154+ wantContent string
155+ }{
156+ {
157+ name : "assistant default role empty delta" ,
158+ chunk : aguievents .NewTextMessageChunkEvent ().WithChunkMessageID ("msg-1" ),
159+ wantRole : "assistant" ,
160+ wantName : testAppName ,
161+ wantContent : "" ,
162+ },
163+ {
164+ name : "user role with delta" ,
165+ chunk : aguievents .NewTextMessageChunkEvent ().
166+ WithChunkMessageID ("msg-2" ).
167+ WithChunkRole ("user" ).
168+ WithChunkDelta ("hi" ),
169+ wantRole : "user" ,
170+ wantName : testUserID ,
171+ wantContent : "hi" ,
172+ },
173+ }
174+ for _ , tt := range tests {
175+ t .Run (tt .name , func (t * testing.T ) {
176+ r := new (testAppName , testUserID )
177+ if err := r .handleTextChunk (tt .chunk ); err != nil {
178+ t .Fatalf ("handleTextChunk err: %v" , err )
179+ }
180+ if err := r .finalize (); err != nil {
181+ t .Fatalf ("finalize err: %v" , err )
182+ }
183+ if len (r .messages ) != 1 {
184+ t .Fatalf ("expected 1 message, got %d" , len (r .messages ))
185+ }
186+ msg := r .messages [0 ]
187+ if msg .Role != tt .wantRole {
188+ t .Fatalf ("unexpected role %q" , msg .Role )
189+ }
190+ if msg .Name == nil || * msg .Name != tt .wantName {
191+ t .Fatalf ("unexpected name %v" , msg .Name )
192+ }
193+ if msg .Content == nil || * msg .Content != tt .wantContent {
194+ t .Fatalf ("unexpected content %v" , msg .Content )
195+ }
196+ state , ok := r .texts [* tt .chunk .MessageID ]
197+ if ! ok {
198+ t .Fatalf ("expected text state for %s" , * tt .chunk .MessageID )
199+ }
200+ if state .phase != textEnded {
201+ t .Fatalf ("unexpected phase %v" , state .phase )
202+ }
203+ if got := state .content .String (); got != tt .wantContent {
204+ t .Fatalf ("unexpected builder content %q" , got )
205+ }
206+ if state .index != 0 {
207+ t .Fatalf ("unexpected state index %d" , state .index )
208+ }
209+ })
210+ }
211+ }
212+
213+ func TestHandleTextChunkErrors (t * testing.T ) {
214+ t .Run ("missing id" , func (t * testing.T ) {
215+ chunk := aguievents .NewTextMessageChunkEvent ()
216+ r := new (testAppName , testUserID )
217+ if err := r .handleTextChunk (chunk ); err == nil || ! strings .Contains (err .Error (), "text message chunk missing id" ) {
218+ t .Fatalf ("unexpected error %v" , err )
219+ }
220+ })
221+ t .Run ("duplicate id" , func (t * testing.T ) {
222+ chunk := aguievents .NewTextMessageChunkEvent ().WithChunkMessageID ("msg-1" )
223+ r := new (testAppName , testUserID )
224+ if err := r .handleTextChunk (chunk ); err != nil {
225+ t .Fatalf ("handleTextChunk err: %v" , err )
226+ }
227+ if err := r .handleTextChunk (chunk ); err == nil || ! strings .Contains (err .Error (), "duplicate text message chunk: msg-1" ) {
228+ t .Fatalf ("unexpected error %v" , err )
229+ }
230+ })
231+ t .Run ("unsupported role" , func (t * testing.T ) {
232+ chunk := aguievents .NewTextMessageChunkEvent ().WithChunkMessageID ("msg-3" ).WithChunkRole ("tool" )
233+ r := new (testAppName , testUserID )
234+ if err := r .handleTextChunk (chunk ); err == nil || ! strings .Contains (err .Error (), "unsupported role: tool" ) {
235+ t .Fatalf ("unexpected error %v" , err )
236+ }
237+ })
238+ t .Run ("empty string id pointer" , func (t * testing.T ) {
239+ chunk := aguievents .NewTextMessageChunkEvent ()
240+ empty := ""
241+ chunk .MessageID = & empty
242+ r := new (testAppName , testUserID )
243+ if err := r .handleTextChunk (chunk ); err == nil || ! strings .Contains (err .Error (), "text message chunk missing id" ) {
244+ t .Fatalf ("unexpected error %v" , err )
245+ }
246+ })
247+ }
248+
249+ func TestReduceEventDispatchesChunk (t * testing.T ) {
250+ r := new (testAppName , testUserID )
251+ chunk := aguievents .NewTextMessageChunkEvent ().WithChunkMessageID ("msg-1" ).WithChunkDelta ("hi" )
252+ if err := r .reduceEvent (chunk ); err != nil {
253+ t .Fatalf ("reduceEvent err: %v" , err )
254+ }
255+ if err := r .finalize (); err != nil {
256+ t .Fatalf ("finalize err: %v" , err )
257+ }
258+ if len (r .messages ) != 1 {
259+ t .Fatalf ("expected 1 message, got %d" , len (r .messages ))
260+ }
261+ if r .messages [0 ].Content == nil || * r .messages [0 ].Content != "hi" {
262+ t .Fatalf ("unexpected content %v" , r .messages [0 ].Content )
263+ }
264+ }
265+
112266func TestAssistantOnlyToolCall (t * testing.T ) {
113267 events := []session.TrackEvent {
114268 newTrackEvent (aguievents .NewTextMessageStartEvent ("assistant-1" , aguievents .WithRole ("assistant" ))),
0 commit comments