2026-02-10 15:37:51 -08:00
import type { ClawdbotConfig , PluginRuntime , RuntimeEnv } from "openclaw/plugin-sdk" ;
import { beforeEach , describe , expect , it , vi } from "vitest" ;
import type { FeishuMessageEvent } from "./bot.js" ;
2026-02-28 12:49:05 +08:00
import { buildFeishuAgentBody , handleFeishuMessage , toMessageResourceType } from "./bot.js" ;
2026-02-10 15:37:51 -08:00
import { setFeishuRuntime } from "./runtime.js" ;
2026-02-22 19:21:01 +01:00
const {
mockCreateFeishuReplyDispatcher ,
mockSendMessageFeishu ,
mockGetMessageFeishu ,
mockDownloadMessageResourceFeishu ,
2026-02-26 13:03:29 +01:00
mockCreateFeishuClient ,
2026-02-28 09:26:36 +08:00
mockResolveAgentRoute ,
2026-02-22 19:21:01 +01:00
} = vi . hoisted ( ( ) = > ( {
mockCreateFeishuReplyDispatcher : vi.fn ( ( ) = > ( {
dispatcher : vi.fn ( ) ,
replyOptions : { } ,
markDispatchIdle : vi.fn ( ) ,
} ) ) ,
mockSendMessageFeishu : vi.fn ( ) . mockResolvedValue ( { messageId : "pairing-msg" , chatId : "oc-dm" } ) ,
mockGetMessageFeishu : vi.fn ( ) . mockResolvedValue ( null ) ,
mockDownloadMessageResourceFeishu : vi.fn ( ) . mockResolvedValue ( {
buffer : Buffer.from ( "video" ) ,
contentType : "video/mp4" ,
fileName : "clip.mp4" ,
2026-02-13 05:43:30 +01:00
} ) ,
2026-02-26 13:03:29 +01:00
mockCreateFeishuClient : vi.fn ( ) ,
2026-02-28 09:26:36 +08:00
mockResolveAgentRoute : vi.fn ( ( ) = > ( {
agentId : "main" ,
accountId : "default" ,
sessionKey : "agent:main:feishu:dm:ou-attacker" ,
matchedBy : "default" ,
} ) ) ,
2026-02-22 19:21:01 +01:00
} ) ) ;
2026-02-10 15:37:51 -08:00
vi . mock ( "./reply-dispatcher.js" , ( ) = > ( {
createFeishuReplyDispatcher : mockCreateFeishuReplyDispatcher ,
} ) ) ;
2026-02-13 05:43:30 +01:00
vi . mock ( "./send.js" , ( ) = > ( {
sendMessageFeishu : mockSendMessageFeishu ,
getMessageFeishu : mockGetMessageFeishu ,
} ) ) ;
2026-02-22 19:21:01 +01:00
vi . mock ( "./media.js" , ( ) = > ( {
downloadMessageResourceFeishu : mockDownloadMessageResourceFeishu ,
} ) ) ;
2026-02-26 13:03:29 +01:00
vi . mock ( "./client.js" , ( ) = > ( {
createFeishuClient : mockCreateFeishuClient ,
} ) ) ;
2026-02-22 11:28:05 +00:00
function createRuntimeEnv ( ) : RuntimeEnv {
return {
log : vi.fn ( ) ,
error : vi.fn ( ) ,
exit : vi.fn ( ( code : number ) : never = > {
throw new Error ( ` exit ${ code } ` ) ;
} ) ,
} as RuntimeEnv ;
}
async function dispatchMessage ( params : { cfg : ClawdbotConfig ; event : FeishuMessageEvent } ) {
await handleFeishuMessage ( {
cfg : params.cfg ,
event : params.event ,
runtime : createRuntimeEnv ( ) ,
} ) ;
}
2026-02-26 13:19:17 +01:00
describe ( "buildFeishuAgentBody" , ( ) = > {
it ( "builds message id, speaker, quoted content, mentions, and permission notice in order" , ( ) = > {
const body = buildFeishuAgentBody ( {
ctx : {
content : "hello world" ,
senderName : "Sender Name" ,
senderOpenId : "ou-sender" ,
messageId : "msg-42" ,
mentionTargets : [ { openId : "ou-target" , name : "Target User" , key : "@_user_1" } ] ,
} ,
quotedContent : "previous message" ,
permissionErrorForAgent : {
code : 99991672 ,
message : "permission denied" ,
grantUrl : "https://open.feishu.cn/app/cli_test" ,
} ,
} ) ;
expect ( body ) . toBe (
'[message_id: msg-42]\nSender Name: [Replying to: "previous message"]\n\nhello world\n\n[System: Your reply will automatically @mention: Target User. Do not write @xxx yourself.]\n\n[System: The bot encountered a Feishu API permission error. Please inform the user about this issue and provide the permission grant URL for the admin to authorize. Permission grant URL: https://open.feishu.cn/app/cli_test]' ,
) ;
} ) ;
} ) ;
2026-02-10 15:37:51 -08:00
describe ( "handleFeishuMessage command authorization" , ( ) = > {
const mockFinalizeInboundContext = vi . fn ( ( ctx : unknown ) = > ctx ) ;
const mockDispatchReplyFromConfig = vi
. fn ( )
. mockResolvedValue ( { queuedFinal : false , counts : { final : 1 } } ) ;
2026-02-26 17:36:09 +01:00
const mockWithReplyDispatcher = vi . fn (
async ( {
dispatcher ,
run ,
onSettled ,
} : Parameters < PluginRuntime [ "channel" ] [ "reply" ] [ "withReplyDispatcher" ] > [ 0 ] ) = > {
try {
return await run ( ) ;
} finally {
dispatcher . markComplete ( ) ;
try {
await dispatcher . waitForIdle ( ) ;
} finally {
await onSettled ? . ( ) ;
}
}
} ,
) ;
2026-02-10 15:37:51 -08:00
const mockResolveCommandAuthorizedFromAuthorizers = vi . fn ( ( ) = > false ) ;
const mockShouldComputeCommandAuthorized = vi . fn ( ( ) = > true ) ;
const mockReadAllowFromStore = vi . fn ( ) . mockResolvedValue ( [ ] ) ;
2026-02-13 05:43:30 +01:00
const mockUpsertPairingRequest = vi . fn ( ) . mockResolvedValue ( { code : "ABCDEFGH" , created : false } ) ;
const mockBuildPairingReply = vi . fn ( ( ) = > "Pairing response" ) ;
2026-03-02 03:33:42 +00:00
const mockEnqueueSystemEvent = vi . fn ( ) ;
2026-02-22 19:21:01 +01:00
const mockSaveMediaBuffer = vi . fn ( ) . mockResolvedValue ( {
path : "/tmp/inbound-clip.mp4" ,
contentType : "video/mp4" ,
} ) ;
2026-02-10 15:37:51 -08:00
beforeEach ( ( ) = > {
vi . clearAllMocks ( ) ;
2026-03-02 03:33:42 +00:00
mockShouldComputeCommandAuthorized . mockReset ( ) . mockReturnValue ( true ) ;
2026-02-28 09:26:36 +08:00
mockResolveAgentRoute . mockReturnValue ( {
agentId : "main" ,
accountId : "default" ,
sessionKey : "agent:main:feishu:dm:ou-attacker" ,
matchedBy : "default" ,
} ) ;
2026-02-26 13:03:29 +01:00
mockCreateFeishuClient . mockReturnValue ( {
contact : {
user : {
get : vi . fn ( ) . mockResolvedValue ( { data : { user : { name : "Sender" } } } ) ,
} ,
} ,
} ) ;
2026-03-02 03:33:42 +00:00
mockEnqueueSystemEvent . mockReset ( ) ;
2026-02-10 15:37:51 -08:00
setFeishuRuntime ( {
system : {
2026-03-02 03:33:42 +00:00
enqueueSystemEvent : mockEnqueueSystemEvent ,
2026-02-10 15:37:51 -08:00
} ,
channel : {
routing : {
2026-02-28 09:26:36 +08:00
resolveAgentRoute : mockResolveAgentRoute ,
2026-02-10 15:37:51 -08:00
} ,
reply : {
resolveEnvelopeFormatOptions : vi.fn ( ( ) = > ( { template : "channel+name+time" } ) ) ,
formatAgentEnvelope : vi.fn ( ( params : { body : string } ) = > params . body ) ,
finalizeInboundContext : mockFinalizeInboundContext ,
dispatchReplyFromConfig : mockDispatchReplyFromConfig ,
2026-02-26 17:36:09 +01:00
withReplyDispatcher : mockWithReplyDispatcher ,
2026-02-10 15:37:51 -08:00
} ,
commands : {
shouldComputeCommandAuthorized : mockShouldComputeCommandAuthorized ,
resolveCommandAuthorizedFromAuthorizers : mockResolveCommandAuthorizedFromAuthorizers ,
} ,
2026-02-22 19:21:01 +01:00
media : {
saveMediaBuffer : mockSaveMediaBuffer ,
} ,
2026-02-10 15:37:51 -08:00
pairing : {
readAllowFromStore : mockReadAllowFromStore ,
2026-02-13 05:43:30 +01:00
upsertPairingRequest : mockUpsertPairingRequest ,
buildPairingReply : mockBuildPairingReply ,
2026-02-10 15:37:51 -08:00
} ,
} ,
2026-02-22 19:21:01 +01:00
media : {
detectMime : vi.fn ( async ( ) = > "application/octet-stream" ) ,
} ,
2026-02-10 15:37:51 -08:00
} as unknown as PluginRuntime ) ;
} ) ;
2026-03-02 03:33:42 +00:00
it ( "does not enqueue inbound preview text as system events" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
dmPolicy : "open" ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-attacker" ,
} ,
} ,
message : {
message_id : "msg-no-system-preview" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "text" ,
content : JSON.stringify ( { text : "hi there" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockEnqueueSystemEvent ) . not . toHaveBeenCalled ( ) ;
} ) ;
2026-02-10 15:37:51 -08:00
it ( "uses authorizer resolution instead of hardcoded CommandAuthorized=true" , async ( ) = > {
const cfg : ClawdbotConfig = {
commands : { useAccessGroups : true } ,
channels : {
feishu : {
2026-02-13 05:43:30 +01:00
dmPolicy : "open" ,
2026-02-10 15:37:51 -08:00
allowFrom : [ "ou-admin" ] ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-attacker" ,
} ,
} ,
message : {
message_id : "msg-auth-bypass-regression" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "text" ,
content : JSON.stringify ( { text : "/status" } ) ,
} ,
} ;
2026-02-22 11:28:05 +00:00
await dispatchMessage ( { cfg , event } ) ;
2026-02-10 15:37:51 -08:00
expect ( mockResolveCommandAuthorizedFromAuthorizers ) . toHaveBeenCalledWith ( {
useAccessGroups : true ,
authorizers : [ { configured : true , allowed : false } ] ,
} ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledWith (
expect . objectContaining ( {
CommandAuthorized : false ,
SenderId : "ou-attacker" ,
Surface : "feishu" ,
} ) ,
) ;
} ) ;
2026-02-13 05:43:30 +01:00
it ( "reads pairing allow store for non-command DMs when dmPolicy is pairing" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
mockReadAllowFromStore . mockResolvedValue ( [ "ou-attacker" ] ) ;
const cfg : ClawdbotConfig = {
commands : { useAccessGroups : true } ,
channels : {
feishu : {
dmPolicy : "pairing" ,
allowFrom : [ ] ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-attacker" ,
} ,
} ,
message : {
message_id : "msg-read-store-non-command" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "text" ,
content : JSON.stringify ( { text : "hello there" } ) ,
} ,
} ;
2026-02-22 11:28:05 +00:00
await dispatchMessage ( { cfg , event } ) ;
2026-02-13 05:43:30 +01:00
2026-02-26 21:24:50 +00:00
expect ( mockReadAllowFromStore ) . toHaveBeenCalledWith ( {
channel : "feishu" ,
accountId : "default" ,
} ) ;
2026-02-13 05:43:30 +01:00
expect ( mockResolveCommandAuthorizedFromAuthorizers ) . not . toHaveBeenCalled ( ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockDispatchReplyFromConfig ) . toHaveBeenCalledTimes ( 1 ) ;
} ) ;
2026-02-28 13:05:54 +08:00
it ( "skips sender-name lookup when resolveSenderNames is false" , async ( ) = > {
const cfg : ClawdbotConfig = {
channels : {
feishu : {
dmPolicy : "open" ,
allowFrom : [ "*" ] ,
resolveSenderNames : false ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-attacker" ,
} ,
} ,
message : {
message_id : "msg-skip-sender-lookup" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "text" ,
content : JSON.stringify ( { text : "hello" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockCreateFeishuClient ) . not . toHaveBeenCalled ( ) ;
} ) ;
2026-02-28 23:55:50 +08:00
it ( "propagates parent/root message ids into inbound context for reply reconstruction" , async ( ) = > {
mockGetMessageFeishu . mockResolvedValueOnce ( {
messageId : "om_parent_001" ,
chatId : "oc-group" ,
content : "quoted content" ,
contentType : "text" ,
} ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
enabled : true ,
dmPolicy : "open" ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-replier" ,
} ,
} ,
message : {
message_id : "om_reply_001" ,
root_id : "om_root_001" ,
parent_id : "om_parent_001" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "text" ,
content : JSON.stringify ( { text : "reply text" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledWith (
expect . objectContaining ( {
ReplyToId : "om_parent_001" ,
RootMessageId : "om_root_001" ,
ReplyToBody : "quoted content" ,
} ) ,
) ;
} ) ;
2026-02-13 05:43:30 +01:00
it ( "creates pairing request and drops unauthorized DMs in pairing mode" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
mockReadAllowFromStore . mockResolvedValue ( [ ] ) ;
mockUpsertPairingRequest . mockResolvedValue ( { code : "ABCDEFGH" , created : true } ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
dmPolicy : "pairing" ,
allowFrom : [ ] ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-unapproved" ,
} ,
} ,
message : {
message_id : "msg-pairing-flow" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "text" ,
content : JSON.stringify ( { text : "hello" } ) ,
} ,
} ;
2026-02-22 11:28:05 +00:00
await dispatchMessage ( { cfg , event } ) ;
2026-02-13 05:43:30 +01:00
expect ( mockUpsertPairingRequest ) . toHaveBeenCalledWith ( {
channel : "feishu" ,
2026-02-26 21:24:50 +00:00
accountId : "default" ,
2026-02-13 05:43:30 +01:00
id : "ou-unapproved" ,
meta : { name : undefined } ,
} ) ;
expect ( mockBuildPairingReply ) . toHaveBeenCalledWith ( {
channel : "feishu" ,
idLine : "Your Feishu user id: ou-unapproved" ,
code : "ABCDEFGH" ,
} ) ;
expect ( mockSendMessageFeishu ) . toHaveBeenCalledWith (
expect . objectContaining ( {
to : "user:ou-unapproved" ,
accountId : "default" ,
} ) ,
) ;
expect ( mockFinalizeInboundContext ) . not . toHaveBeenCalled ( ) ;
expect ( mockDispatchReplyFromConfig ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( "computes group command authorization from group allowFrom" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( true ) ;
mockResolveCommandAuthorizedFromAuthorizers . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
commands : { useAccessGroups : true } ,
channels : {
feishu : {
groups : {
"oc-group" : {
requireMention : false ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-attacker" ,
} ,
} ,
message : {
message_id : "msg-group-command-auth" ,
chat_id : "oc-group" ,
chat_type : "group" ,
message_type : "text" ,
content : JSON.stringify ( { text : "/status" } ) ,
} ,
} ;
2026-02-22 11:28:05 +00:00
await dispatchMessage ( { cfg , event } ) ;
2026-02-13 05:43:30 +01:00
expect ( mockResolveCommandAuthorizedFromAuthorizers ) . toHaveBeenCalledWith ( {
useAccessGroups : true ,
authorizers : [ { configured : false , allowed : false } ] ,
} ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledWith (
expect . objectContaining ( {
ChatType : "group" ,
CommandAuthorized : false ,
SenderId : "ou-attacker" ,
} ) ,
) ;
} ) ;
2026-02-22 19:13:04 +01:00
it ( "falls back to top-level allowFrom for group command authorization" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( true ) ;
mockResolveCommandAuthorizedFromAuthorizers . mockReturnValue ( true ) ;
const cfg : ClawdbotConfig = {
commands : { useAccessGroups : true } ,
channels : {
feishu : {
allowFrom : [ "ou-admin" ] ,
groups : {
"oc-group" : {
requireMention : false ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-admin" ,
} ,
} ,
message : {
message_id : "msg-group-command-fallback" ,
chat_id : "oc-group" ,
chat_type : "group" ,
message_type : "text" ,
content : JSON.stringify ( { text : "/status" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockResolveCommandAuthorizedFromAuthorizers ) . toHaveBeenCalledWith ( {
useAccessGroups : true ,
authorizers : [ { configured : true , allowed : true } ] ,
} ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledWith (
expect . objectContaining ( {
ChatType : "group" ,
CommandAuthorized : true ,
SenderId : "ou-admin" ,
} ) ,
) ;
} ) ;
2026-02-22 19:21:01 +01:00
2026-02-27 19:49:47 -08:00
it ( "allows group sender when global groupSenderAllowFrom includes sender" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
groupPolicy : "open" ,
groupSenderAllowFrom : [ "ou-allowed" ] ,
groups : {
"oc-group" : {
requireMention : false ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-allowed" ,
} ,
} ,
message : {
message_id : "msg-global-group-sender-allow" ,
chat_id : "oc-group" ,
chat_type : "group" ,
message_type : "text" ,
content : JSON.stringify ( { text : "hello" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledWith (
expect . objectContaining ( {
ChatType : "group" ,
SenderId : "ou-allowed" ,
} ) ,
) ;
expect ( mockDispatchReplyFromConfig ) . toHaveBeenCalledTimes ( 1 ) ;
} ) ;
it ( "blocks group sender when global groupSenderAllowFrom excludes sender" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
groupPolicy : "open" ,
groupSenderAllowFrom : [ "ou-allowed" ] ,
groups : {
"oc-group" : {
requireMention : false ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-blocked" ,
} ,
} ,
message : {
message_id : "msg-global-group-sender-block" ,
chat_id : "oc-group" ,
chat_type : "group" ,
message_type : "text" ,
content : JSON.stringify ( { text : "hello" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockFinalizeInboundContext ) . not . toHaveBeenCalled ( ) ;
expect ( mockDispatchReplyFromConfig ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( "prefers per-group allowFrom over global groupSenderAllowFrom" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
groupPolicy : "open" ,
groupSenderAllowFrom : [ "ou-global" ] ,
groups : {
"oc-group" : {
allowFrom : [ "ou-group-only" ] ,
requireMention : false ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-global" ,
} ,
} ,
message : {
message_id : "msg-per-group-precedence" ,
chat_id : "oc-group" ,
chat_type : "group" ,
2026-02-28 13:39:21 +08:00
message_type : "text" ,
content : JSON.stringify ( { text : "hello" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockFinalizeInboundContext ) . not . toHaveBeenCalled ( ) ;
expect ( mockDispatchReplyFromConfig ) . not . toHaveBeenCalled ( ) ;
} ) ;
it ( "drops message when groupConfig.enabled is false" , async ( ) = > {
const cfg : ClawdbotConfig = {
channels : {
feishu : {
groups : {
"oc-disabled-group" : {
enabled : false ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : { open_id : "ou-sender" } ,
} ,
message : {
message_id : "msg-disabled-group" ,
chat_id : "oc-disabled-group" ,
chat_type : "group" ,
2026-02-27 19:49:47 -08:00
message_type : "text" ,
content : JSON.stringify ( { text : "hello" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockFinalizeInboundContext ) . not . toHaveBeenCalled ( ) ;
expect ( mockDispatchReplyFromConfig ) . not . toHaveBeenCalled ( ) ;
} ) ;
2026-02-22 19:21:01 +01:00
it ( "uses video file_key (not thumbnail image_key) for inbound video download" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
dmPolicy : "open" ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-sender" ,
} ,
} ,
message : {
message_id : "msg-video-inbound" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "video" ,
content : JSON.stringify ( {
file_key : "file_video_payload" ,
image_key : "img_thumb_payload" ,
file_name : "clip.mp4" ,
} ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockDownloadMessageResourceFeishu ) . toHaveBeenCalledWith (
expect . objectContaining ( {
messageId : "msg-video-inbound" ,
fileKey : "file_video_payload" ,
type : "file" ,
} ) ,
) ;
expect ( mockSaveMediaBuffer ) . toHaveBeenCalledWith (
expect . any ( Buffer ) ,
2026-02-28 12:28:37 +08:00
"video/mp4" ,
"inbound" ,
expect . any ( Number ) ,
"clip.mp4" ,
) ;
} ) ;
it ( "uses media message_type file_key (not thumbnail image_key) for inbound mobile video download" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
dmPolicy : "open" ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-sender" ,
} ,
} ,
message : {
message_id : "msg-media-inbound" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "media" ,
content : JSON.stringify ( {
file_key : "file_media_payload" ,
image_key : "img_media_thumb" ,
file_name : "mobile.mp4" ,
} ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockDownloadMessageResourceFeishu ) . toHaveBeenCalledWith (
expect . objectContaining ( {
messageId : "msg-media-inbound" ,
fileKey : "file_media_payload" ,
type : "file" ,
} ) ,
) ;
expect ( mockSaveMediaBuffer ) . toHaveBeenCalledWith (
expect . any ( Buffer ) ,
2026-02-22 19:21:01 +01:00
"video/mp4" ,
"inbound" ,
expect . any ( Number ) ,
"clip.mp4" ,
) ;
} ) ;
2026-02-26 13:01:46 +01:00
2026-02-28 13:39:24 +08:00
it ( "downloads embedded media tags from post messages as files" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
dmPolicy : "open" ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-sender" ,
} ,
} ,
message : {
message_id : "msg-post-media" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "post" ,
content : JSON.stringify ( {
title : "Rich text" ,
content : [
[
{
tag : "media" ,
file_key : "file_post_media_payload" ,
file_name : "embedded.mov" ,
} ,
] ,
] ,
} ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockDownloadMessageResourceFeishu ) . toHaveBeenCalledWith (
expect . objectContaining ( {
messageId : "msg-post-media" ,
fileKey : "file_post_media_payload" ,
type : "file" ,
} ) ,
) ;
expect ( mockSaveMediaBuffer ) . toHaveBeenCalledWith (
expect . any ( Buffer ) ,
"video/mp4" ,
"inbound" ,
expect . any ( Number ) ,
) ;
} ) ;
2026-02-26 13:01:46 +01:00
it ( "includes message_id in BodyForAgent on its own line" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
dmPolicy : "open" ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-msgid" ,
} ,
} ,
message : {
message_id : "msg-message-id-line" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "text" ,
content : JSON.stringify ( { text : "hello" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledWith (
expect . objectContaining ( {
BodyForAgent : "[message_id: msg-message-id-line]\nou-msgid: hello" ,
} ) ,
) ;
} ) ;
2026-02-26 13:03:29 +01:00
2026-02-28 10:57:18 +08:00
it ( "expands merge_forward content from API sub-messages" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const mockGetMerged = vi . fn ( ) . mockResolvedValue ( {
code : 0 ,
data : {
items : [
{
message_id : "container" ,
msg_type : "merge_forward" ,
body : { content : JSON.stringify ( { text : "Merged and Forwarded Message" } ) } ,
} ,
{
message_id : "sub-2" ,
upper_message_id : "container" ,
msg_type : "file" ,
body : { content : JSON.stringify ( { file_name : "report.pdf" } ) } ,
create_time : "2000" ,
} ,
{
message_id : "sub-1" ,
upper_message_id : "container" ,
msg_type : "text" ,
body : { content : JSON.stringify ( { text : "alpha" } ) } ,
create_time : "1000" ,
} ,
] ,
} ,
} ) ;
mockCreateFeishuClient . mockReturnValue ( {
contact : {
user : {
get : vi . fn ( ) . mockResolvedValue ( { data : { user : { name : "Sender" } } } ) ,
} ,
} ,
im : {
message : {
get : mockGetMerged ,
} ,
} ,
} ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
dmPolicy : "open" ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-merge" ,
} ,
} ,
message : {
message_id : "msg-merge-forward" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "merge_forward" ,
content : JSON.stringify ( { text : "Merged and Forwarded Message" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockGetMerged ) . toHaveBeenCalledWith ( {
path : { message_id : "msg-merge-forward" } ,
} ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledWith (
expect . objectContaining ( {
BodyForAgent : expect.stringContaining (
"[Merged and Forwarded Messages]\n- alpha\n- [File: report.pdf]" ,
) ,
} ) ,
) ;
} ) ;
it ( "falls back when merge_forward API returns no sub-messages" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
mockCreateFeishuClient . mockReturnValue ( {
contact : {
user : {
get : vi . fn ( ) . mockResolvedValue ( { data : { user : { name : "Sender" } } } ) ,
} ,
} ,
im : {
message : {
get : vi . fn ( ) . mockResolvedValue ( { code : 0 , data : { items : [ ] } } ) ,
} ,
} ,
} ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
dmPolicy : "open" ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-merge-empty" ,
} ,
} ,
message : {
message_id : "msg-merge-empty" ,
chat_id : "oc-dm" ,
chat_type : "p2p" ,
message_type : "merge_forward" ,
content : JSON.stringify ( { text : "Merged and Forwarded Message" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledWith (
expect . objectContaining ( {
BodyForAgent : expect.stringContaining ( "[Merged and Forwarded Message - could not fetch]" ) ,
} ) ,
) ;
} ) ;
2026-02-26 13:03:29 +01:00
it ( "dispatches once and appends permission notice to the main agent body" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
mockCreateFeishuClient . mockReturnValue ( {
contact : {
user : {
get : vi . fn ( ) . mockRejectedValue ( {
response : {
data : {
code : 99991672 ,
msg : "permission denied https://open.feishu.cn/app/cli_test" ,
} ,
} ,
} ) ,
} ,
} ,
} ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
appId : "cli_test" ,
appSecret : "sec_test" ,
groups : {
"oc-group" : {
requireMention : false ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : {
sender_id : {
open_id : "ou-perm" ,
} ,
} ,
message : {
message_id : "msg-perm-1" ,
chat_id : "oc-group" ,
chat_type : "group" ,
message_type : "text" ,
content : JSON.stringify ( { text : "hello group" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockDispatchReplyFromConfig ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledWith (
expect . objectContaining ( {
BodyForAgent : expect.stringContaining (
"Permission grant URL: https://open.feishu.cn/app/cli_test" ,
) ,
} ) ,
) ;
expect ( mockFinalizeInboundContext ) . toHaveBeenCalledWith (
expect . objectContaining ( {
BodyForAgent : expect.stringContaining ( "ou-perm: hello group" ) ,
} ) ,
) ;
} ) ;
2026-02-28 09:26:36 +08:00
it ( "routes group sessions by sender when groupSessionScope=group_sender" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
groups : {
"oc-group" : {
requireMention : false ,
groupSessionScope : "group_sender" ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : { sender_id : { open_id : "ou-scope-user" } } ,
message : {
message_id : "msg-scope-group-sender" ,
chat_id : "oc-group" ,
chat_type : "group" ,
message_type : "text" ,
content : JSON.stringify ( { text : "group sender scope" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockResolveAgentRoute ) . toHaveBeenCalledWith (
expect . objectContaining ( {
peer : { kind : "group" , id : "oc-group:sender:ou-scope-user" } ,
parentPeer : null ,
} ) ,
) ;
} ) ;
it ( "routes topic sessions and parentPeer when groupSessionScope=group_topic_sender" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
groups : {
"oc-group" : {
requireMention : false ,
groupSessionScope : "group_topic_sender" ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : { sender_id : { open_id : "ou-topic-user" } } ,
message : {
message_id : "msg-scope-topic-sender" ,
chat_id : "oc-group" ,
chat_type : "group" ,
root_id : "om_root_topic" ,
message_type : "text" ,
content : JSON.stringify ( { text : "topic sender scope" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockResolveAgentRoute ) . toHaveBeenCalledWith (
expect . objectContaining ( {
peer : { kind : "group" , id : "oc-group:topic:om_root_topic:sender:ou-topic-user" } ,
parentPeer : { kind : "group" , id : "oc-group" } ,
} ) ,
) ;
} ) ;
it ( "maps legacy topicSessionMode=enabled to group_topic routing" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
topicSessionMode : "enabled" ,
groups : {
"oc-group" : {
requireMention : false ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : { sender_id : { open_id : "ou-legacy" } } ,
message : {
message_id : "msg-legacy-topic-mode" ,
chat_id : "oc-group" ,
chat_type : "group" ,
root_id : "om_root_legacy" ,
message_type : "text" ,
content : JSON.stringify ( { text : "legacy topic mode" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockResolveAgentRoute ) . toHaveBeenCalledWith (
expect . objectContaining ( {
peer : { kind : "group" , id : "oc-group:topic:om_root_legacy" } ,
parentPeer : { kind : "group" , id : "oc-group" } ,
} ) ,
) ;
} ) ;
2026-02-28 09:53:02 +08:00
it ( "uses message_id as topic root when group_topic + replyInThread and no root_id" , async ( ) = > {
mockShouldComputeCommandAuthorized . mockReturnValue ( false ) ;
const cfg : ClawdbotConfig = {
channels : {
feishu : {
groups : {
"oc-group" : {
requireMention : false ,
groupSessionScope : "group_topic" ,
replyInThread : "enabled" ,
} ,
} ,
} ,
} ,
} as ClawdbotConfig ;
const event : FeishuMessageEvent = {
sender : { sender_id : { open_id : "ou-topic-init" } } ,
message : {
message_id : "msg-new-topic-root" ,
chat_id : "oc-group" ,
chat_type : "group" ,
message_type : "text" ,
content : JSON.stringify ( { text : "create topic" } ) ,
} ,
} ;
await dispatchMessage ( { cfg , event } ) ;
expect ( mockResolveAgentRoute ) . toHaveBeenCalledWith (
expect . objectContaining ( {
peer : { kind : "group" , id : "oc-group:topic:msg-new-topic-root" } ,
parentPeer : { kind : "group" , id : "oc-group" } ,
} ) ,
) ;
} ) ;
2026-02-10 15:37:51 -08:00
} ) ;
2026-02-28 12:49:05 +08:00
describe ( "toMessageResourceType" , ( ) = > {
it ( "maps image to image" , ( ) = > {
expect ( toMessageResourceType ( "image" ) ) . toBe ( "image" ) ;
} ) ;
it ( "maps audio to file" , ( ) = > {
expect ( toMessageResourceType ( "audio" ) ) . toBe ( "file" ) ;
} ) ;
it ( "maps video/file/sticker to file" , ( ) = > {
expect ( toMessageResourceType ( "video" ) ) . toBe ( "file" ) ;
expect ( toMessageResourceType ( "file" ) ) . toBe ( "file" ) ;
expect ( toMessageResourceType ( "sticker" ) ) . toBe ( "file" ) ;
} ) ;
} ) ;