@@ -129,28 +129,16 @@ describe("streamAgent – context in POST body", () => {
129129// ─── SSE streaming behaviour ──────────────────────────────────────────────────
130130
131131describe ( "streamAgent – SSE streaming" , ( ) => {
132- it ( "text-delta chunk calls onChunk with the delta string " , async ( ) => {
132+ it ( "text-delta chunk does NOT call onChunk (answer suppressed until finish) " , async ( ) => {
133133 const onChunk = vi . fn ( )
134- await streamAgent (
135- "Q?" ,
136- makeOpts ( {
137- onChunk,
138- ...await makeSseResponse ( [
139- { type : "text-delta" , textDelta : "Hello!" } ,
140- { type : "finish-message" } ,
141- ] ) . then ( ( ) => ( { } ) ) ,
142- } )
143- )
144- // Use a dedicated fetch mock for this test
145- const onChunk2 = vi . fn ( )
146134 mockFetch . mockImplementationOnce ( ( ) =>
147135 makeSseResponse ( [
148136 { type : "text-delta" , textDelta : "Hello!" } ,
149137 { type : "finish-message" } ,
150138 ] )
151139 )
152- await streamAgent ( "Q?" , makeOpts ( { onChunk : onChunk2 } ) )
153- expect ( onChunk2 ) . toHaveBeenCalledWith ( "Hello!" )
140+ await streamAgent ( "Q?" , makeOpts ( { onChunk } ) )
141+ expect ( onChunk ) . not . toHaveBeenCalled ( )
154142 } )
155143
156144 it ( "tool-input-available calls onToolCall with status in-flight" , async ( ) => {
@@ -200,23 +188,92 @@ describe("streamAgent – SSE streaming", () => {
200188 expect ( done ) . toBeDefined ( )
201189 } )
202190
203- it ( "finish-message calls onDone with accumulated text and empty cited_ref_ids" , async ( ) => {
191+ it ( "finish-message parses JSON envelope: extracts answer and cited_ref_ids" , async ( ) => {
204192 const onDone = vi . fn ( )
193+ const envelope = JSON . stringify ( {
194+ answer : "The answer is 42." ,
195+ cited_ref_ids : [ "node-abc" , "node-xyz" ] ,
196+ usage : { } ,
197+ } )
205198 mockFetch . mockImplementationOnce ( ( ) =>
206199 makeSseResponse ( [
207- { type : "text-delta" , textDelta : "Foo " } ,
208- { type : "text-delta" , textDelta : "bar." } ,
200+ { type : "text-delta" , textDelta : envelope } ,
209201 { type : "finish-message" } ,
210202 ] )
211203 )
212204 await streamAgent ( "Q?" , makeOpts ( { onDone } ) )
213- expect ( onDone ) . toHaveBeenCalledWith ( { answer : "Foo bar." , cited_ref_ids : [ ] } )
205+ expect ( onDone ) . toHaveBeenCalledWith ( {
206+ answer : "The answer is 42." ,
207+ cited_ref_ids : [ "node-abc" , "node-xyz" ] ,
208+ } )
209+ } )
210+
211+ it ( "finish-message: envelope missing cited_ref_ids defaults to []" , async ( ) => {
212+ const onDone = vi . fn ( )
213+ const envelope = JSON . stringify ( { answer : "Short answer." } )
214+ mockFetch . mockImplementationOnce ( ( ) =>
215+ makeSseResponse ( [
216+ { type : "text-delta" , textDelta : envelope } ,
217+ { type : "finish-message" } ,
218+ ] )
219+ )
220+ await streamAgent ( "Q?" , makeOpts ( { onDone } ) )
221+ expect ( onDone ) . toHaveBeenCalledWith ( { answer : "Short answer." , cited_ref_ids : [ ] } )
222+ } )
223+
224+ it ( "finish-message: strips end-of-answer marker before parsing" , async ( ) => {
225+ const onDone = vi . fn ( )
226+ const envelope =
227+ JSON . stringify ( { answer : "Marked answer." , cited_ref_ids : [ "ref-1" ] } ) +
228+ "[END_OF_ANSWER]"
229+ mockFetch . mockImplementationOnce ( ( ) =>
230+ makeSseResponse ( [
231+ { type : "text-delta" , textDelta : envelope } ,
232+ { type : "finish-message" } ,
233+ ] )
234+ )
235+ await streamAgent ( "Q?" , makeOpts ( { onDone } ) )
236+ expect ( onDone ) . toHaveBeenCalledWith ( { answer : "Marked answer." , cited_ref_ids : [ "ref-1" ] } )
237+ } )
238+
239+ it ( "finish-message: JSON inside a ```json fence is still extracted" , async ( ) => {
240+ const onDone = vi . fn ( )
241+ const fenced =
242+ "```json\n" +
243+ JSON . stringify ( { answer : "Fenced answer." , cited_ref_ids : [ "ref-2" ] } ) +
244+ "\n```"
245+ mockFetch . mockImplementationOnce ( ( ) =>
246+ makeSseResponse ( [
247+ { type : "text-delta" , textDelta : fenced } ,
248+ { type : "finish-message" } ,
249+ ] )
250+ )
251+ await streamAgent ( "Q?" , makeOpts ( { onDone } ) )
252+ expect ( onDone ) . toHaveBeenCalledWith ( { answer : "Fenced answer." , cited_ref_ids : [ "ref-2" ] } )
253+ } )
254+
255+ it ( "finish-message: falls back to raw text and warns on invalid JSON" , async ( ) => {
256+ const onDone = vi . fn ( )
257+ const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
258+ mockFetch . mockImplementationOnce ( ( ) =>
259+ makeSseResponse ( [
260+ { type : "text-delta" , textDelta : "{ bad json }" } ,
261+ { type : "finish-message" } ,
262+ ] )
263+ )
264+ await streamAgent ( "Q?" , makeOpts ( { onDone } ) )
265+ expect ( warnSpy ) . toHaveBeenCalledWith (
266+ expect . stringContaining ( "[agent-api] envelope parse failed" )
267+ )
268+ expect ( onDone ) . toHaveBeenCalledWith ( { answer : "{ bad json }" , cited_ref_ids : [ ] } )
269+ warnSpy . mockRestore ( )
214270 } )
215271
216- it ( "fallback onDone called when stream ends without finish-message" , async ( ) => {
272+ it ( "fallback onDone called when stream ends without finish-message (parses envelope) " , async ( ) => {
217273 const onDone = vi . fn ( )
274+ const envelope = JSON . stringify ( { answer : "Partial." , cited_ref_ids : [ ] } )
218275 mockFetch . mockImplementationOnce ( ( ) =>
219- makeSseResponse ( [ { type : "text-delta" , textDelta : "Partial." } ] )
276+ makeSseResponse ( [ { type : "text-delta" , textDelta : envelope } ] )
220277 )
221278 await streamAgent ( "Q?" , makeOpts ( { onDone } ) )
222279 expect ( onDone ) . toHaveBeenCalledWith ( { answer : "Partial." , cited_ref_ids : [ ] } )
0 commit comments