import type { EnvelopeFormatOptions } from "openclaw/plugin-sdk/channel-inbound" ;
import { beforeEach, describe, expect, it, vi } from "vitest" ;
import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js" ;
import type { FeishuMessageEvent } from "./bot.js" ;
import { handleFeishuMessage } from "./bot.js" ;
import { setFeishuRuntime } from "./runtime.js" ;
const { mockCreateFeishuReplyDispatcher, mockCreateFeishuClient, mockResolveAgentRoute } =
vi.hoisted(() => ({
mockCreateFeishuReplyDispatcher: vi.fn(() => ({
dispatcher: {
sendToolResult: vi.fn(),
sendBlockReply: vi.fn(),
sendFinalReply: vi.fn(),
waitForIdle: vi.fn(),
getQueuedCounts: vi.fn(() => ({ tool: 0 , block: 0 , final : 0 })),
getFailedCounts: vi.fn(() => ({ tool: 0 , block: 0 , final : 0 })),
markComplete: vi.fn(),
},
replyOptions: {},
markDispatchIdle: vi.fn(),
})),
mockCreateFeishuClient: vi.fn(),
mockResolveAgentRoute: vi.fn(),
}));
vi.mock("./reply-dispatcher.js" , () => ({
createFeishuReplyDispatcher: mockCreateFeishuReplyDispatcher,
}));
vi.mock("./client.js" , () => ({
createFeishuClient: mockCreateFeishuClient,
}));
function createRuntimeEnv() {
return {
log: vi.fn(),
error: vi.fn(),
writeStdout: vi.fn(),
writeJson: vi.fn(),
exit: vi.fn((code: number): never => {
throw new Error(`exit ${code}`);
}),
};
}
describe("broadcast dispatch" , () => {
const finalizeInboundContextCalls: Array<Record<string, unknown>> = [];
const mockFinalizeInboundContext: PluginRuntime["channel" ]["reply" ]["finalizeInboundContext" ] = (
ctx,
) => {
finalizeInboundContextCalls.push(ctx);
return {
...ctx,
CommandAuthorized: typeof ctx.CommandAuthorized === "boolean" ? ctx.CommandAuthorized : false ,
};
};
const mockDispatchReplyFromConfig = vi
.fn()
.mockResolvedValue({ queuedFinal: false , counts: { final : 1 } });
const mockWithReplyDispatcher: PluginRuntime["channel" ]["reply" ]["withReplyDispatcher" ] = async ({
dispatcher,
run,
onSettled,
}) => {
try {
return await run();
} finally {
dispatcher.markComplete();
try {
await dispatcher.waitForIdle();
} finally {
await onSettled?.();
}
}
};
const resolveEnvelopeFormatOptionsMock: PluginRuntime["channel" ]["reply" ]["resolveEnvelopeFormatOptions" ] =
() => ({}) satisfies EnvelopeFormatOptions;
const mockShouldComputeCommandAuthorized = vi.fn(() => false );
const mockSaveMediaBuffer = vi.fn().mockResolvedValue({
path: "/tmp/inbound-clip.mp4" ,
contentType: "video/mp4" ,
});
const runtimeStub = {
system: {
enqueueSystemEvent: vi.fn(),
},
channel: {
routing: {
resolveAgentRoute: (params: unknown) => mockResolveAgentRoute(params),
},
session: {
resolveStorePath: vi.fn(() => "/tmp/feishu-session-store.json" ),
},
reply: {
resolveEnvelopeFormatOptions: resolveEnvelopeFormatOptionsMock,
formatAgentEnvelope: vi.fn((params: { body: string }) => params.body),
finalizeInboundContext:
mockFinalizeInboundContext as unknown as PluginRuntime["channel" ]["reply" ]["finalizeInboundContext" ],
dispatchReplyFromConfig: mockDispatchReplyFromConfig,
withReplyDispatcher:
mockWithReplyDispatcher as unknown as PluginRuntime["channel" ]["reply" ]["withReplyDispatcher" ],
},
commands: {
shouldComputeCommandAuthorized: mockShouldComputeCommandAuthorized,
resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false ),
},
media: {
saveMediaBuffer: mockSaveMediaBuffer,
},
pairing: {
readAllowFromStore: vi.fn().mockResolvedValue([]),
upsertPairingRequest: vi.fn().mockResolvedValue({ code: "ABCDEFGH" , created: false }),
buildPairingReply: vi.fn(() => "Pairing response" ),
},
},
media: {
detectMime: vi.fn(async () => "application/octet-stream" ),
},
} as unknown as PluginRuntime;
function createBroadcastConfig(): ClawdbotConfig {
return {
broadcast: { "oc-broadcast-group" : ["susan" , "main" ] },
agents: { list: [{ id: "main" }, { id: "susan" }] },
channels: {
feishu: {
groups: {
"oc-broadcast-group" : {
requireMention: true ,
},
},
},
},
};
}
function createBroadcastEvent(options: {
messageId: string;
text: string;
botMentioned?: boolean ;
}): FeishuMessageEvent {
return {
sender: { sender_id: { open_id: "ou-sender" } },
message: {
message_id: options.messageId,
chat_id: "oc-broadcast-group" ,
chat_type: "group" ,
message_type: "text" ,
content: JSON.stringify({ text: options.text }),
...(options.botMentioned
? {
mentions: [
{
key: "@_user_1" ,
id: { open_id: "bot-open-id" },
name: "Bot" ,
tenant_key: "" ,
},
],
}
: {}),
},
};
}
beforeEach(() => {
vi.clearAllMocks();
finalizeInboundContextCalls.length = 0 ;
mockResolveAgentRoute.mockReturnValue({
agentId: "main" ,
channel: "feishu" ,
accountId: "default" ,
sessionKey: "agent:main:feishu:group:oc-broadcast-group" ,
mainSessionKey: "agent:main:main" ,
lastRoutePolicy: "session" ,
matchedBy: "default" ,
});
mockCreateFeishuClient.mockReturnValue({
contact: {
user: {
get: vi.fn().mockResolvedValue({ data: { user: { name: "Sender" } } }),
},
},
});
setFeishuRuntime(runtimeStub);
});
it("dispatches to all broadcast agents when bot is mentioned" , async () => {
const cfg = createBroadcastConfig();
const event = createBroadcastEvent({
messageId: "msg-broadcast-mentioned" ,
text: "hello @bot" ,
botMentioned: true ,
});
await handleFeishuMessage({
cfg,
event,
botOpenId: "bot-open-id" ,
runtime: createRuntimeEnv(),
});
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(2 );
const sessionKeys = finalizeInboundContextCalls.map((call) => call.SessionKey);
expect(sessionKeys).toContain("agent:susan:feishu:group:oc-broadcast-group" );
expect(sessionKeys).toContain("agent:main:feishu:group:oc-broadcast-group" );
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledTimes(1 );
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
expect.objectContaining({ agentId: "main" }),
);
});
it("skips broadcast dispatch when bot is NOT mentioned (requireMention=true)" , async () => {
const cfg = createBroadcastConfig();
const event = createBroadcastEvent({
messageId: "msg-broadcast-not-mentioned" ,
text: "hello everyone" ,
});
await handleFeishuMessage({
cfg,
event,
botOpenId: "ou_known_bot" ,
runtime: createRuntimeEnv(),
});
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
expect(mockCreateFeishuReplyDispatcher).not.toHaveBeenCalled();
});
it("skips broadcast dispatch when bot identity is unknown (requireMention=true)" , async () => {
const cfg = createBroadcastConfig();
const event = createBroadcastEvent({
messageId: "msg-broadcast-unknown-bot-id" ,
text: "hello everyone" ,
});
await handleFeishuMessage({
cfg,
event,
runtime: createRuntimeEnv(),
});
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
expect(mockCreateFeishuReplyDispatcher).not.toHaveBeenCalled();
});
it("preserves single-agent dispatch when no broadcast config" , async () => {
const cfg: ClawdbotConfig = {
channels: {
feishu: {
groups: {
"oc-broadcast-group" : {
requireMention: false ,
},
},
},
},
};
const event: FeishuMessageEvent = {
sender: { sender_id: { open_id: "ou-sender" } },
message: {
message_id: "msg-no-broadcast" ,
chat_id: "oc-broadcast-group" ,
chat_type: "group" ,
message_type: "text" ,
content: JSON.stringify({ text: "hello" }),
},
};
await handleFeishuMessage({
cfg,
event,
runtime: createRuntimeEnv(),
});
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1 );
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledTimes(1 );
expect(finalizeInboundContextCalls).toContainEqual(
expect.objectContaining({
SessionKey: "agent:main:feishu:group:oc-broadcast-group" ,
}),
);
});
it("cross-account broadcast dedup: second account skips dispatch" , async () => {
const cfg: ClawdbotConfig = {
broadcast: { "oc-broadcast-group" : ["susan" , "main" ] },
agents: { list: [{ id: "main" }, { id: "susan" }] },
channels: {
feishu: {
groups: {
"oc-broadcast-group" : {
requireMention: false ,
},
},
},
},
};
const event: FeishuMessageEvent = {
sender: { sender_id: { open_id: "ou-sender" } },
message: {
message_id: "msg-multi-account-dedup" ,
chat_id: "oc-broadcast-group" ,
chat_type: "group" ,
message_type: "text" ,
content: JSON.stringify({ text: "hello" }),
},
};
await handleFeishuMessage({
cfg,
event,
runtime: createRuntimeEnv(),
accountId: "account-A" ,
});
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(2 );
mockDispatchReplyFromConfig.mockClear();
finalizeInboundContextCalls.length = 0 ;
await handleFeishuMessage({
cfg,
event,
runtime: createRuntimeEnv(),
accountId: "account-B" ,
});
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
});
it("skips unknown agents not in agents.list" , async () => {
const cfg: ClawdbotConfig = {
broadcast: { "oc-broadcast-group" : ["susan" , "unknown-agent" ] },
agents: { list: [{ id: "main" }, { id: "susan" }] },
channels: {
feishu: {
groups: {
"oc-broadcast-group" : {
requireMention: false ,
},
},
},
},
};
const event: FeishuMessageEvent = {
sender: { sender_id: { open_id: "ou-sender" } },
message: {
message_id: "msg-broadcast-unknown-agent" ,
chat_id: "oc-broadcast-group" ,
chat_type: "group" ,
message_type: "text" ,
content: JSON.stringify({ text: "hello" }),
},
};
await handleFeishuMessage({
cfg,
event,
runtime: createRuntimeEnv(),
});
expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1 );
const sessionKey =
typeof finalizeInboundContextCalls[0 ]?.SessionKey === "string"
? finalizeInboundContextCalls[0 ].SessionKey
: "" ;
expect(sessionKey).toBe("agent:susan:feishu:group:oc-broadcast-group" );
});
});
Messung V0.5 in Prozent C=96 H=95 G=95
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland