openclaw/extensions/whatsapp/src/auto-reply.broadcast-groups.combined.test.ts
scoootscooob 16505718e8
refactor: move WhatsApp channel implementation to extensions/ (#45725)
* refactor: move WhatsApp channel from src/web/ to extensions/whatsapp/

Move all WhatsApp implementation code (77 source/test files + 9 channel
plugin files) from src/web/ and src/channels/plugins/*/whatsapp* to
extensions/whatsapp/src/.

- Leave thin re-export shims at all original locations so cross-cutting
  imports continue to resolve
- Update plugin-sdk/whatsapp.ts to only re-export generic framework
  utilities; channel-specific functions imported locally by the extension
- Update vi.mock paths in 15 cross-cutting test files
- Rename outbound.ts -> send.ts to match extension naming conventions
  and avoid false positive in cfg-threading guard test
- Widen tsconfig.plugin-sdk.dts.json rootDir to support shim->extension
  cross-directory references

Part of the core-channels-to-extensions migration (PR 6/10).

* style: format WhatsApp extension files

* fix: correct stale import paths in WhatsApp extension tests

Fix vi.importActual, test mock, and hardcoded source paths that weren't
updated during the file move:
- media.test.ts: vi.importActual path
- onboarding.test.ts: vi.importActual path
- test-helpers.ts: test/mocks/baileys.js path
- monitor-inbox.test-harness.ts: incomplete media/store mock
- login.test.ts: hardcoded source file path
- message-action-runner.media.test.ts: vi.mock/importActual path
2026-03-14 02:44:55 -07:00

195 lines
5.4 KiB
TypeScript

import "./test-helpers.js";
import { describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../../src/config/config.js";
import {
monitorWebChannelWithCapture,
sendWebDirectInboundAndCollectSessionKeys,
} from "./auto-reply.broadcast-groups.test-harness.js";
import {
installWebAutoReplyTestHomeHooks,
installWebAutoReplyUnitTestHooks,
resetLoadConfigMock,
sendWebGroupInboundMessage,
setLoadConfigMock,
} from "./auto-reply.test-harness.js";
installWebAutoReplyTestHomeHooks();
describe("broadcast groups", () => {
installWebAutoReplyUnitTestHooks();
it("skips unknown broadcast agent ids when agents.list is present", async () => {
setLoadConfigMock({
channels: { whatsapp: { allowFrom: ["*"] } },
agents: {
defaults: { maxConcurrent: 10 },
list: [{ id: "alfred" }],
},
broadcast: {
"+1000": ["alfred", "missing"],
},
} satisfies OpenClawConfig);
const { seen, resolver } = await sendWebDirectInboundAndCollectSessionKeys();
expect(resolver).toHaveBeenCalledTimes(1);
expect(seen[0]).toContain("agent:alfred:");
resetLoadConfigMock();
});
it("broadcasts sequentially in configured order", async () => {
setLoadConfigMock({
channels: { whatsapp: { allowFrom: ["*"] } },
agents: {
defaults: { maxConcurrent: 10 },
list: [{ id: "alfred" }, { id: "baerbel" }],
},
broadcast: {
strategy: "sequential",
"+1000": ["alfred", "baerbel"],
},
} satisfies OpenClawConfig);
const { seen, resolver } = await sendWebDirectInboundAndCollectSessionKeys();
expect(resolver).toHaveBeenCalledTimes(2);
expect(seen[0]).toContain("agent:alfred:");
expect(seen[1]).toContain("agent:baerbel:");
resetLoadConfigMock();
});
it("shares group history across broadcast agents and clears after replying", async () => {
setLoadConfigMock({
channels: { whatsapp: { allowFrom: ["*"] } },
agents: {
defaults: { maxConcurrent: 10 },
list: [{ id: "alfred" }, { id: "baerbel" }],
},
broadcast: {
strategy: "sequential",
"123@g.us": ["alfred", "baerbel"],
},
} satisfies OpenClawConfig);
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
const { spies, onMessage } = await monitorWebChannelWithCapture(resolver);
await sendWebGroupInboundMessage({
onMessage,
spies,
body: "hello group",
id: "g1",
senderE164: "+111",
senderName: "Alice",
selfE164: "+999",
});
expect(resolver).not.toHaveBeenCalled();
await sendWebGroupInboundMessage({
onMessage,
spies,
body: "@bot ping",
id: "g2",
senderE164: "+222",
senderName: "Bob",
mentionedJids: ["999@s.whatsapp.net"],
selfE164: "+999",
selfJid: "999@s.whatsapp.net",
});
expect(resolver).toHaveBeenCalledTimes(2);
for (const call of resolver.mock.calls.slice(0, 2)) {
const payload = call[0] as {
Body: string;
SenderName?: string;
SenderE164?: string;
SenderId?: string;
};
expect(payload.Body).toContain("Chat messages since your last reply");
expect(payload.Body).toContain("Alice (+111): hello group");
expect(payload.Body).not.toContain("[message_id:");
expect(payload.Body).toContain("@bot ping");
expect(payload.SenderName).toBe("Bob");
expect(payload.SenderE164).toBe("+222");
expect(payload.SenderId).toBe("+222");
}
await sendWebGroupInboundMessage({
onMessage,
spies,
body: "@bot ping 2",
id: "g3",
senderE164: "+333",
senderName: "Clara",
mentionedJids: ["999@s.whatsapp.net"],
selfE164: "+999",
selfJid: "999@s.whatsapp.net",
});
expect(resolver).toHaveBeenCalledTimes(4);
for (const call of resolver.mock.calls.slice(2, 4)) {
const payload = call[0] as { Body: string };
expect(payload.Body).not.toContain("Alice (+111): hello group");
expect(payload.Body).not.toContain("Chat messages since your last reply");
}
resetLoadConfigMock();
});
it("broadcasts in parallel by default", async () => {
setLoadConfigMock({
channels: { whatsapp: { allowFrom: ["*"] } },
agents: {
defaults: { maxConcurrent: 10 },
list: [{ id: "alfred" }, { id: "baerbel" }],
},
broadcast: {
strategy: "parallel",
"+1000": ["alfred", "baerbel"],
},
} satisfies OpenClawConfig);
const sendMedia = vi.fn();
const reply = vi.fn().mockResolvedValue(undefined);
const sendComposing = vi.fn();
let started = 0;
let release: (() => void) | undefined;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const resolver = vi.fn(async () => {
started += 1;
if (started < 2) {
await gate;
} else {
release?.();
}
return { text: "ok" };
});
const { onMessage: capturedOnMessage } = await monitorWebChannelWithCapture(resolver);
await capturedOnMessage({
id: "m1",
from: "+1000",
conversationId: "+1000",
to: "+2000",
accountId: "default",
body: "hello",
timestamp: Date.now(),
chatType: "direct",
chatId: "direct:+1000",
sendComposing,
reply,
sendMedia,
});
expect(resolver).toHaveBeenCalledTimes(2);
resetLoadConfigMock();
});
});