import "./test-helpers.js"; import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { monitorWebChannel } from "./auto-reply.js"; import { createWebInboundDeliverySpies, createWebListenerFactoryCapture, installWebAutoReplyTestHomeHooks, installWebAutoReplyUnitTestHooks, resetLoadConfigMock, sendWebDirectInboundMessage, sendWebGroupInboundMessage, setLoadConfigMock, } from "./auto-reply.test-harness.js"; installWebAutoReplyTestHomeHooks(); describe("broadcast groups", () => { installWebAutoReplyUnitTestHooks(); 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: string[] = []; const resolver = vi.fn(async (ctx: { SessionKey?: unknown }) => { seen.push(String(ctx.SessionKey)); return { text: "ok" }; }); const spies = createWebInboundDeliverySpies(); const { listenerFactory, getOnMessage } = createWebListenerFactoryCapture(); await monitorWebChannel(false, listenerFactory, false, resolver); const onMessage = getOnMessage(); expect(onMessage).toBeDefined(); await sendWebDirectInboundMessage({ onMessage: onMessage!, spies, id: "m1", from: "+1000", to: "+2000", body: "hello", }); 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 spies = createWebInboundDeliverySpies(); const resolver = vi.fn().mockResolvedValue({ text: "ok" }); const { listenerFactory, getOnMessage } = createWebListenerFactoryCapture(); await monitorWebChannel(false, listenerFactory, false, resolver); const onMessage = getOnMessage(); expect(onMessage).toBeDefined(); await sendWebGroupInboundMessage({ onMessage: onMessage!, spies, body: "hello group", id: "g1", senderE164: "+111", senderName: "Alice", selfE164: "+999", }); expect(resolver).not.toHaveBeenCalled(); await sendWebGroupInboundMessage({ onMessage: 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"); // Message id hints are not included in prompts anymore. 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: 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((resolve) => { release = resolve; }); const resolver = vi.fn(async () => { started += 1; if (started < 2) { await gate; } else { release?.(); } return { text: "ok" }; }); let capturedOnMessage: | ((msg: import("./inbound.js").WebInboundMessage) => Promise) | undefined; const listenerFactory = async (opts: { onMessage: (msg: import("./inbound.js").WebInboundMessage) => Promise; }) => { capturedOnMessage = opts.onMessage; return { close: vi.fn() }; }; await monitorWebChannel(false, listenerFactory, false, resolver); expect(capturedOnMessage).toBeDefined(); await capturedOnMessage?.({ id: "m1", from: "+1000", conversationId: "+1000", to: "+2000", body: "hello", timestamp: Date.now(), chatType: "direct", chatId: "direct:+1000", sendComposing, reply, sendMedia, }); expect(resolver).toHaveBeenCalledTimes(2); resetLoadConfigMock(); }); });