fix(whatsapp): allow media-only sends and normalize leading blank payloads (#14408)

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Karim Naguib
2026-02-11 21:21:21 -08:00
committed by GitHub
parent 186dc0363f
commit 7a0591ef87
11 changed files with 352 additions and 14 deletions

View File

@@ -9,7 +9,11 @@ import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
import { whatsappPlugin } from "../../../extensions/whatsapp/src/channel.js";
import { jsonResult } from "../../agents/tools/common.js";
import { setActivePluginRegistry } from "../../plugins/runtime.js";
import { createIMessageTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js";
import {
createIMessageTestPlugin,
createOutboundTestPlugin,
createTestRegistry,
} from "../../test-utils/channel-plugins.js";
import { loadWebMedia } from "../../web/media.js";
import { runMessageAction } from "./message-action-runner.js";
@@ -609,6 +613,152 @@ describe("runMessageAction sandboxed media validation", () => {
});
});
describe("runMessageAction media caption behavior", () => {
afterEach(() => {
setActivePluginRegistry(createTestRegistry([]));
});
it("promotes caption to message for media sends when message is empty", async () => {
const sendMedia = vi.fn().mockResolvedValue({
channel: "testchat",
messageId: "m1",
chatId: "c1",
});
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "testchat",
source: "test",
plugin: createOutboundTestPlugin({
id: "testchat",
outbound: {
deliveryMode: "direct",
sendText: vi.fn().mockResolvedValue({
channel: "testchat",
messageId: "t1",
chatId: "c1",
}),
sendMedia,
},
}),
},
]),
);
const cfg = {
channels: {
testchat: {
enabled: true,
},
},
} as OpenClawConfig;
const result = await runMessageAction({
cfg,
action: "send",
params: {
channel: "testchat",
target: "channel:abc",
media: "https://example.com/cat.png",
caption: "caption-only text",
},
dryRun: false,
});
expect(result.kind).toBe("send");
expect(sendMedia).toHaveBeenCalledWith(
expect.objectContaining({
text: "caption-only text",
mediaUrl: "https://example.com/cat.png",
}),
);
});
});
describe("runMessageAction card-only send behavior", () => {
const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
jsonResult({
ok: true,
card: params.card ?? null,
message: params.message ?? null,
}),
);
const cardPlugin: ChannelPlugin = {
id: "cardchat",
meta: {
id: "cardchat",
label: "Card Chat",
selectionLabel: "Card Chat",
docsPath: "/channels/cardchat",
blurb: "Card-only send test plugin.",
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: () => ["default"],
resolveAccount: () => ({ enabled: true }),
isConfigured: () => true,
},
actions: {
listActions: () => ["send"],
supportsAction: ({ action }) => action === "send",
handleAction,
},
};
beforeEach(() => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "cardchat",
source: "test",
plugin: cardPlugin,
},
]),
);
handleAction.mockClear();
});
afterEach(() => {
setActivePluginRegistry(createTestRegistry([]));
vi.clearAllMocks();
});
it("allows card-only sends without text or media", async () => {
const cfg = {
channels: {
cardchat: {
enabled: true,
},
},
} as OpenClawConfig;
const card = {
type: "AdaptiveCard",
version: "1.4",
body: [{ type: "TextBlock", text: "Card-only payload" }],
};
const result = await runMessageAction({
cfg,
action: "send",
params: {
channel: "cardchat",
target: "channel:test-card",
card,
},
dryRun: false,
});
expect(result.kind).toBe("send");
expect(result.handledBy).toBe("plugin");
expect(handleAction).toHaveBeenCalled();
expect(result.payload).toMatchObject({
ok: true,
card,
});
});
});
describe("runMessageAction accountId defaults", () => {
const handleAction = vi.fn(async () => jsonResult({ ok: true }));
const accountPlugin: ChannelPlugin = {