test(whatsapp): add context isolation coverage

Includes outbound gating, threading fallback, and web auto-reply context assertions.
This commit is contained in:
Peter Steinberger
2026-01-14 23:23:28 +00:00
parent a70937c926
commit fd41000bc3
3 changed files with 97 additions and 0 deletions

View File

@@ -23,6 +23,21 @@ describe("buildThreadingToolContext", () => {
expect(result.currentChannelId).toBe("123@g.us");
});
it("falls back to To for WhatsApp when From is missing", () => {
const sessionCtx = {
Provider: "whatsapp",
To: "+15550001",
} as TemplateContext;
const result = buildThreadingToolContext({
sessionCtx,
config: cfg,
hasRepliedRef: undefined,
});
expect(result.currentChannelId).toBe("+15550001");
});
it("uses the recipient id for other channels", () => {
const sessionCtx = {
Provider: "telegram",

View File

@@ -12,6 +12,14 @@ const slackConfig = {
},
} as ClawdbotConfig;
const whatsappConfig = {
channels: {
whatsapp: {
allowFrom: ["*"],
},
},
} as ClawdbotConfig;
describe("runMessageAction context isolation", () => {
it("allows send when target matches current channel", async () => {
const result = await runMessageAction({
@@ -60,4 +68,36 @@ describe("runMessageAction context isolation", () => {
}),
).rejects.toThrow(/Cross-context messaging denied/);
});
it("allows WhatsApp send when target matches current chat", async () => {
const result = await runMessageAction({
cfg: whatsappConfig,
action: "send",
params: {
channel: "whatsapp",
to: "group:123@g.us",
message: "hi",
},
toolContext: { currentChannelId: "123@g.us" },
dryRun: true,
});
expect(result.kind).toBe("send");
});
it("blocks WhatsApp send when target differs from current chat", async () => {
await expect(
runMessageAction({
cfg: whatsappConfig,
action: "send",
params: {
channel: "whatsapp",
to: "456@g.us",
message: "hi",
},
toolContext: { currentChannelId: "123@g.us" },
dryRun: true,
}),
).rejects.toThrow(/Cross-context messaging denied/);
});
});

View File

@@ -168,6 +168,48 @@ describe("web auto-reply", () => {
expect(payload.Body).toContain("@bot ping");
expect(payload.Body).toContain("[from: Bob (+222)]");
});
it("passes conversation id through as From for group replies", async () => {
const sendMedia = vi.fn();
const reply = vi.fn().mockResolvedValue(undefined);
const sendComposing = vi.fn();
const resolver = vi.fn().mockResolvedValue({ 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?.({
body: "@bot ping",
from: "123@g.us",
conversationId: "123@g.us",
chatId: "123@g.us",
chatType: "group",
to: "+2",
id: "g1",
senderE164: "+222",
senderName: "Bob",
mentionedJids: ["999@s.whatsapp.net"],
selfE164: "+999",
selfJid: "999@s.whatsapp.net",
sendComposing,
reply,
sendMedia,
});
const payload = resolver.mock.calls[0]?.[0] as { From?: string; To?: string };
expect(payload.From).toBe("123@g.us");
expect(payload.To).toBe("+2");
});
it("detects LID mentions using authDir mapping", async () => {
const sendMedia = vi.fn();
const reply = vi.fn().mockResolvedValue(undefined);