211 lines
6.0 KiB
TypeScript
211 lines
6.0 KiB
TypeScript
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<void>((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<void>)
|
|
| undefined;
|
|
const listenerFactory = async (opts: {
|
|
onMessage: (msg: import("./inbound.js").WebInboundMessage) => Promise<void>;
|
|
}) => {
|
|
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();
|
|
});
|
|
});
|