@@ -2108,4 +2108,129 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
21082108 nowSpy . mockRestore ( ) ;
21092109 }
21102110 } ) ;
2111+
2112+ it ( "skips reply anchor for ordinary DM with streaming enabled (fixes #94922)" , async ( ) => {
2113+ // Arrange: Ordinary DM session (not group, not thread reply)
2114+ // skipReplyToInMessages=true simulates what bot.ts calculates for P2P DMs
2115+ resolveFeishuAccountMock . mockReturnValue ( {
2116+ accountId : "main" ,
2117+ appId : "app_id" ,
2118+ appSecret : "app_secret" ,
2119+ domain : "feishu" ,
2120+ config : {
2121+ renderMode : "auto" ,
2122+ streaming : true ,
2123+ } ,
2124+ } ) ;
2125+
2126+ createFeishuReplyDispatcher ( {
2127+ cfg : { } as never ,
2128+ agentId : "agent" ,
2129+ runtime : { } as never ,
2130+ chatId : "oc_dm_chat" ,
2131+ replyToMessageId : "om_parent" ,
2132+ skipReplyToInMessages : true , // Key fix: DM should skip reply anchor
2133+ replyInThread : false ,
2134+ threadReply : false ,
2135+ } ) ;
2136+
2137+ const options = firstTypingDispatcherOptions ( ) ;
2138+
2139+ // Act: Send a streaming reply
2140+ await options . deliver ( { text : "DM reply without reply anchor" } , { kind : "final" } ) ;
2141+ await options . onIdle ?.( ) ;
2142+
2143+ // Assert: streaming.start should be called with replyToMessageId: undefined
2144+ expect ( streamingInstances ) . toHaveLength ( 1 ) ;
2145+ const startOptions = firstMockArg ( streamingInstances [ 0 ] . start , "streaming start" , 2 ) as Record <
2146+ string ,
2147+ unknown
2148+ > ;
2149+ expect ( startOptions . replyToMessageId ) . toBeUndefined ( ) ;
2150+ expect ( startOptions . replyInThread ) . toBe ( false ) ;
2151+ } ) ;
2152+
2153+ it ( "preserves reply anchor for group topic with streaming enabled" , async ( ) => {
2154+ // Arrange: Group topic session
2155+ resolveFeishuAccountMock . mockReturnValue ( {
2156+ accountId : "main" ,
2157+ appId : "app_id" ,
2158+ appSecret : "app_secret" ,
2159+ domain : "feishu" ,
2160+ config : {
2161+ renderMode : "auto" ,
2162+ streaming : true ,
2163+ } ,
2164+ } ) ;
2165+
2166+ createFeishuReplyDispatcher ( {
2167+ cfg : { } as never ,
2168+ agentId : "agent" ,
2169+ runtime : { } as never ,
2170+ chatId : "oc_group_chat" ,
2171+ replyToMessageId : "om_topic_root" ,
2172+ rootId : "om_topic_root" ,
2173+ isGroup : true ,
2174+ directThreadReply : false ,
2175+ replyInThread : true ,
2176+ } ) ;
2177+
2178+ const options = firstTypingDispatcherOptions ( ) ;
2179+
2180+ // Act: Send a streaming reply to group topic
2181+ await options . deliver ( { text : "Group topic reply" } , { kind : "final" } ) ;
2182+ await options . onIdle ?.( ) ;
2183+
2184+ // Assert: streaming.start should preserve replyToMessageId for group topics
2185+ expect ( streamingInstances ) . toHaveLength ( 1 ) ;
2186+ const startOptions = firstMockArg ( streamingInstances [ 0 ] . start , "streaming start" , 2 ) as Record <
2187+ string ,
2188+ unknown
2189+ > ;
2190+ expect ( startOptions . replyToMessageId ) . toBe ( "om_topic_root" ) ;
2191+ expect ( startOptions . rootId ) . toBe ( "om_topic_root" ) ;
2192+ expect ( startOptions . replyInThread ) . toBe ( true ) ;
2193+ } ) ;
2194+
2195+ it ( "preserves reply anchor for P2P thread reply with streaming enabled" , async ( ) => {
2196+ // Arrange: P2P thread reply session
2197+ resolveFeishuAccountMock . mockReturnValue ( {
2198+ accountId : "main" ,
2199+ appId : "app_id" ,
2200+ appSecret : "app_secret" ,
2201+ domain : "feishu" ,
2202+ config : {
2203+ renderMode : "auto" ,
2204+ streaming : true ,
2205+ } ,
2206+ } ) ;
2207+
2208+ createFeishuReplyDispatcher ( {
2209+ cfg : { } as never ,
2210+ agentId : "agent" ,
2211+ runtime : { } as never ,
2212+ chatId : "oc_dm_thread" ,
2213+ replyToMessageId : "om_dm_thread_root" ,
2214+ rootId : "om_dm_thread_root" ,
2215+ isGroup : false ,
2216+ directThreadReply : true ,
2217+ threadReply : true ,
2218+ } ) ;
2219+
2220+ const options = firstTypingDispatcherOptions ( ) ;
2221+
2222+ // Act: Send a streaming reply in P2P thread
2223+ await options . deliver ( { text : "P2P thread reply" } , { kind : "final" } ) ;
2224+ await options . onIdle ?.( ) ;
2225+
2226+ // Assert: streaming.start should preserve replyToMessageId for P2P threads
2227+ expect ( streamingInstances ) . toHaveLength ( 1 ) ;
2228+ const startOptions = firstMockArg ( streamingInstances [ 0 ] . start , "streaming start" , 2 ) as Record <
2229+ string ,
2230+ unknown
2231+ > ;
2232+ expect ( startOptions . replyToMessageId ) . toBe ( "om_dm_thread_root" ) ;
2233+ expect ( startOptions . rootId ) . toBe ( "om_dm_thread_root" ) ;
2234+ expect ( startOptions . replyInThread ) . toBe ( true ) ;
2235+ } ) ;
21112236} ) ;
0 commit comments