@@ -361,20 +361,25 @@ describe("parseOpenAIStream", () => {
361361
362362 await parseOpenAIStream ( reader , ( e ) => events . push ( e ) , controller . signal ) ;
363363
364- expect ( events ) . toHaveLength ( 3 ) ;
364+ expect ( events ) . toHaveLength ( 4 ) ;
365365 expect ( events [ 0 ] . type ) . toBe ( "tool_call_start" ) ;
366366 if ( events [ 0 ] . type === "tool_call_start" ) {
367367 expect ( events [ 0 ] . toolCall . name ) . toBe ( "dom_read_page" ) ;
368- expect ( events [ 0 ] . toolCall . arguments ) . toBe ( '{"tabId":123' ) ;
368+ // 新行为:start 事件的 args 永远为空,首 chunk 的 args 通过 delta 发出
369+ expect ( events [ 0 ] . toolCall . arguments ) . toBe ( "" ) ;
369370 }
370- // 关键:最后的 tool_call_delta 不应被 usage 检查吞掉
371371 expect ( events [ 1 ] . type ) . toBe ( "tool_call_delta" ) ;
372372 if ( events [ 1 ] . type === "tool_call_delta" ) {
373- expect ( events [ 1 ] . delta ) . toBe ( ',"mode":"summary"} ' ) ;
373+ expect ( events [ 1 ] . delta ) . toBe ( '{"tabId":123 ' ) ; // 故意的 — 模拟 streaming 还没收完的状态
374374 }
375- expect ( events [ 2 ] . type ) . toBe ( "done" ) ;
376- if ( events [ 2 ] . type === "done" ) {
377- expect ( events [ 2 ] . usage ) . toEqual ( { inputTokens : 40010 , outputTokens : 154 } ) ;
375+ // 关键:最后的 tool_call_delta 不应被 usage 检查吞掉
376+ expect ( events [ 2 ] . type ) . toBe ( "tool_call_delta" ) ;
377+ if ( events [ 2 ] . type === "tool_call_delta" ) {
378+ expect ( events [ 2 ] . delta ) . toBe ( ',"mode":"summary"}' ) ;
379+ }
380+ expect ( events [ 3 ] . type ) . toBe ( "done" ) ;
381+ if ( events [ 3 ] . type === "done" ) {
382+ expect ( events [ 3 ] . usage ) . toEqual ( { inputTokens : 40010 , outputTokens : 154 } ) ;
378383 }
379384 } ) ;
380385
@@ -529,13 +534,84 @@ describe("parseOpenAIStream", () => {
529534
530535 await parseOpenAIStream ( reader , ( e ) => events . push ( e ) , controller . signal ) ;
531536
532- expect ( events ) . toHaveLength ( 4 ) ;
537+ expect ( events ) . toHaveLength ( 5 ) ;
533538 expect ( events [ 0 ] ) . toEqual ( { type : "thinking_delta" , delta : "分析页面" } ) ;
534539 expect ( events [ 1 ] ) . toEqual ( { type : "thinking_delta" , delta : "结构" } ) ;
535540 expect ( events [ 2 ] . type ) . toBe ( "tool_call_start" ) ;
536- expect ( events [ 3 ] . type ) . toBe ( "done" ) ;
537- if ( events [ 3 ] . type === "done" ) {
538- expect ( events [ 3 ] . usage ) . toEqual ( { inputTokens : 500 , outputTokens : 50 } ) ;
541+ if ( events [ 2 ] . type === "tool_call_start" ) {
542+ expect ( events [ 2 ] . toolCall . name ) . toBe ( "dom_read_page" ) ;
543+ expect ( events [ 2 ] . toolCall . arguments ) . toBe ( "" ) ;
544+ }
545+ expect ( events [ 3 ] . type ) . toBe ( "tool_call_delta" ) ;
546+ if ( events [ 3 ] . type === "tool_call_delta" ) {
547+ expect ( events [ 3 ] . delta ) . toBe ( '{"selector":".item"}' ) ;
548+ }
549+ expect ( events [ 4 ] . type ) . toBe ( "done" ) ;
550+ if ( events [ 4 ] . type === "done" ) {
551+ expect ( events [ 4 ] . usage ) . toEqual ( { inputTokens : 500 , outputTokens : 50 } ) ;
539552 }
540553 } ) ;
554+
555+ it ( "首 chunk 同时带 name 和 arguments 时:start 事件 args 为空,首 chunk args 作为 delta 发出" , async ( ) => {
556+ const reader = createMockReader ( [
557+ // gateway / 某些 model 会先发一个 arguments="{}" 占位再送真正 JSON
558+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_x","function":{"name":"agent","arguments":"{}"}}]}}]}\n\n' ,
559+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"description\\":\\"r\\""}}]}}]}\n\n' ,
560+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":",\\"prompt\\":\\"do\\"}"}}]}}]}\n\n' ,
561+ "data: [DONE]\n\n" ,
562+ ] ) ;
563+ const events : ChatStreamEvent [ ] = [ ] ;
564+ await parseOpenAIStream ( reader , ( e ) => events . push ( e ) , new AbortController ( ) . signal ) ;
565+
566+ expect ( events [ 0 ] . type ) . toBe ( "tool_call_start" ) ;
567+ if ( events [ 0 ] . type === "tool_call_start" ) {
568+ // 关键断言:start 事件里的 args 必须为空,不能是 "{}"(避免前缀污染)
569+ expect ( events [ 0 ] . toolCall . arguments ) . toBe ( "" ) ;
570+ expect ( events [ 0 ] . toolCall . name ) . toBe ( "agent" ) ;
571+ }
572+ // 首 chunk 的 "{}" 作为第一段 delta 原样透传(模型问题:整体非合法 JSON,但解析器不吞字符)
573+ const deltas = events . filter ( ( e ) => e . type === "tool_call_delta" ) ;
574+ expect ( deltas ) . toHaveLength ( 3 ) ;
575+ expect ( deltas [ 0 ] . type === "tool_call_delta" && deltas [ 0 ] . delta ) . toBe ( "{}" ) ;
576+ expect ( deltas [ 1 ] . type === "tool_call_delta" && deltas [ 1 ] . delta ) . toBe ( '{"description":"r"' ) ;
577+ expect ( deltas [ 2 ] . type === "tool_call_delta" && deltas [ 2 ] . delta ) . toBe ( ',"prompt":"do"}' ) ;
578+ } ) ;
579+
580+ it ( "并发多个 tool_call(不同 index)arguments 不应互相串扰" , async ( ) => {
581+ const reader = createMockReader ( [
582+ // 两个 tool 同时开始
583+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"a","function":{"name":"f1","arguments":""}}]}}]}\n\n' ,
584+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":1,"id":"b","function":{"name":"f2","arguments":""}}]}}]}\n\n' ,
585+ // 然后交错发 arguments delta(只带 index,不带 id)
586+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"x\\":1}"}}]}}]}\n\n' ,
587+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\\"y\\":2}"}}]}}]}\n\n' ,
588+ "data: [DONE]\n\n" ,
589+ ] ) ;
590+ const events : ChatStreamEvent [ ] = [ ] ;
591+ await parseOpenAIStream ( reader , ( e ) => events . push ( e ) , new AbortController ( ) . signal ) ;
592+ // 基础断言:两个 start + 两个 delta + done
593+ const starts = events . filter ( ( e ) => e . type === "tool_call_start" ) ;
594+ expect ( starts ) . toHaveLength ( 2 ) ;
595+ // (完整的 index 匹配需要 ChatStreamEvent 增加 index 字段,这里先确保 parser 不丢 event)
596+ } ) ;
597+
598+ it ( "并行 tool_call 按 index 正确分派 arguments" , async ( ) => {
599+ const reader = createMockReader ( [
600+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"a","function":{"name":"f1","arguments":""}}]}}]}\n\n' ,
601+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":1,"id":"b","function":{"name":"f2","arguments":""}}]}}]}\n\n' ,
602+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\\"y\\":2}"}}]}}]}\n\n' ,
603+ 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\\"x\\":1}"}}]}}]}\n\n' ,
604+ "data: [DONE]\n\n" ,
605+ ] ) ;
606+ const events : ChatStreamEvent [ ] = [ ] ;
607+ await parseOpenAIStream ( reader , ( e ) => events . push ( e ) , new AbortController ( ) . signal ) ;
608+
609+ const deltas = events . filter ( ( e ) => e . type === "tool_call_delta" ) ;
610+ expect ( deltas ) . toHaveLength ( 2 ) ;
611+ // 第一个 delta 对应 index=1(因为到达顺序)
612+ expect ( ( deltas [ 0 ] as any ) . index ) . toBe ( 1 ) ;
613+ expect ( ( deltas [ 0 ] as any ) . delta ) . toBe ( '{"y":2}' ) ;
614+ expect ( ( deltas [ 1 ] as any ) . index ) . toBe ( 0 ) ;
615+ expect ( ( deltas [ 1 ] as any ) . delta ) . toBe ( '{"x":1}' ) ;
616+ } ) ;
541617} ) ;
0 commit comments