@@ -22,6 +22,7 @@ const mockState = vi.hoisted(() => ({
2222 getDriveItemProperties : vi . fn ( ) ,
2323 buildTeamsFileInfoCard : vi . fn ( ) ,
2424 createMSTeamsTokenProvider : vi . fn ( ) ,
25+ uploadAndShareOneDrive : vi . fn ( ) ,
2526} ) ) ;
2627
2728// `loadOutboundMediaFromUrl` is re-exported from msteams's runtime-api which
@@ -86,7 +87,7 @@ vi.mock("./runtime.js", () => ({
8687vi . mock ( "./graph-upload.js" , ( ) => ( {
8788 uploadAndShareSharePoint : mockState . uploadAndShareSharePoint ,
8889 getDriveItemProperties : mockState . getDriveItemProperties ,
89- uploadAndShareOneDrive : vi . fn ( ) ,
90+ uploadAndShareOneDrive : mockState . uploadAndShareOneDrive ,
9091} ) ) ;
9192
9293vi . mock ( "./graph-chat.js" , ( ) => ( {
@@ -238,6 +239,7 @@ describe("sendMessageMSTeams", () => {
238239 mockState . uploadAndShareSharePoint . mockReset ( ) ;
239240 mockState . getDriveItemProperties . mockReset ( ) ;
240241 mockState . buildTeamsFileInfoCard . mockReset ( ) ;
242+ mockState . uploadAndShareOneDrive . mockReset ( ) ;
241243
242244 mockState . extractFilename . mockResolvedValue ( "fallback.bin" ) ;
243245 mockState . requiresFileConsent . mockReturnValue ( false ) ;
@@ -382,6 +384,46 @@ describe("sendMessageMSTeams", () => {
382384 expect ( firstObjectArg ( mockState . sendMSTeamsMessages ) . replyStyle ) . toBe ( "top-level" ) ;
383385 } ) ;
384386
387+ it ( "keeps SharePoint file-card replies inside channel threads" , async ( ) => {
388+ mockState . resolveMSTeamsSendContext . mockResolvedValue ( {
389+ app : createMockApp ( ) ,
390+ appId : "app-id" ,
391+ conversationId :
"19:[email protected] " , 392+ graphChatId :
"19:[email protected] " , 393+ ref : {
394+ activityId : "current-msg-id" ,
395+ threadId : "thread-root-1" ,
396+ conversation :
{ id :
"19:[email protected] " , conversationType :
"channel" } , 397+ } ,
398+ log : { debug : vi . fn ( ) , info : vi . fn ( ) , warn : vi . fn ( ) , error : vi . fn ( ) } ,
399+ conversationType : "channel" ,
400+ replyStyle : "thread" ,
401+ sdkCloudOptions : { cloud : "Public" } ,
402+ tokenProvider : { getAccessToken : vi . fn ( async ( ) => "token" ) } ,
403+ mediaMaxBytes : 8 * 1024 * 1024 ,
404+ sharePointSiteId : "site-threaded" ,
405+ } ) ;
406+ mockSharePointPdfUpload ( {
407+ bufferSize : 100 ,
408+ fileName : "threaded.pdf" ,
409+ itemId : "item-threaded" ,
410+ uniqueId : "{GUID-THREAD}" ,
411+ } ) ;
412+
413+ await sendMessageMSTeams ( {
414+ cfg : { } as OpenClawConfig ,
415+ to :
"conversation:19:[email protected] " , 416+ text : "threaded file" ,
417+ mediaUrl : "https://example.com/threaded.pdf" ,
418+ } ) ;
419+
420+ expect ( mockState . sendMSTeamsActivityWithReference ) . toHaveBeenCalledTimes ( 1 ) ;
421+ expect ( mockState . sendMSTeamsActivityWithReference . mock . calls [ 0 ] ?. [ 3 ] ) . toMatchObject ( {
422+ threadActivityId : "thread-root-1" ,
423+ serviceUrlBoundary : { cloud : "Public" } ,
424+ } ) ;
425+ } ) ;
426+
385427 it ( "uses graphChatId instead of conversationId when uploading to SharePoint" , async ( ) => {
386428 // Simulates a group chat where Bot Framework conversationId is valid but we have
387429 // a resolved Graph chat ID cached from a prior send.
@@ -415,6 +457,51 @@ describe("sendMessageMSTeams", () => {
415457 expect ( uploadPayload . siteId ) . toBe ( "site-123" ) ;
416458 } ) ;
417459
460+ it ( "keeps OneDrive fallback replies inside channel threads" , async ( ) => {
461+ mockState . resolveMSTeamsSendContext . mockResolvedValue ( {
462+ app : createMockApp ( ) ,
463+ appId : "app-id" ,
464+ conversationId :
"19:[email protected] " , 465+ ref : {
466+ activityId : "current-msg-id" ,
467+ threadId : "thread-root-2" ,
468+ conversation :
{ id :
"19:[email protected] " , conversationType :
"channel" } , 469+ } ,
470+ log : { debug : vi . fn ( ) , info : vi . fn ( ) , warn : vi . fn ( ) , error : vi . fn ( ) } ,
471+ conversationType : "channel" ,
472+ replyStyle : "thread" ,
473+ sdkCloudOptions : { cloud : "Public" } ,
474+ tokenProvider : { getAccessToken : vi . fn ( async ( ) => "token" ) } ,
475+ mediaMaxBytes : 8 * 1024 * 1024 ,
476+ sharePointSiteId : undefined ,
477+ } ) ;
478+ mockState . loadOutboundMediaFromUrl . mockResolvedValueOnce ( {
479+ buffer : Buffer . from ( "doc" ) ,
480+ contentType : "application/pdf" ,
481+ fileName : "fallback.pdf" ,
482+ kind : "file" ,
483+ } ) ;
484+ mockState . uploadAndShareOneDrive . mockResolvedValue ( {
485+ itemId : "onedrive-item-1" ,
486+ webUrl : "https://onedrive.example.com/item-1" ,
487+ shareUrl : "https://onedrive.example.com/share/item-1" ,
488+ name : "fallback.pdf" ,
489+ } ) ;
490+
491+ await sendMessageMSTeams ( {
492+ cfg : { } as OpenClawConfig ,
493+ to :
"conversation:19:[email protected] " , 494+ text : "fallback file" ,
495+ mediaUrl : "https://example.com/fallback.pdf" ,
496+ } ) ;
497+
498+ expect ( mockState . sendMSTeamsActivityWithReference ) . toHaveBeenCalledTimes ( 1 ) ;
499+ expect ( mockState . sendMSTeamsActivityWithReference . mock . calls [ 0 ] ?. [ 3 ] ) . toMatchObject ( {
500+ threadActivityId : "thread-root-2" ,
501+ serviceUrlBoundary : { cloud : "Public" } ,
502+ } ) ;
503+ } ) ;
504+
418505 it ( "falls back to conversationId when graphChatId is not available" , async ( ) => {
419506 const botFrameworkConversationId = "19:[email protected] " ; 420507
0 commit comments