import { vi } from "vitest"; vi.mock("../media/store.js", () => ({ saveMediaBuffer: vi.fn().mockResolvedValue({ id: "mid", path: "/tmp/mid", size: 1, contentType: "image/jpeg", }), })); const mockLoadConfig = vi.fn().mockReturnValue({ channels: { whatsapp: { // Allow all in tests by default allowFrom: ["*"], }, }, messages: { messagePrefix: undefined, responsePrefix: undefined, }, }); const readAllowFromStoreMock = vi.fn().mockResolvedValue([]); const upsertPairingRequestMock = vi.fn().mockResolvedValue({ code: "PAIRCODE", created: true }); vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadConfig: () => mockLoadConfig(), }; }); vi.mock("../pairing/pairing-store.js", () => ({ readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args), upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), })); vi.mock("./session.js", () => { const { EventEmitter } = require("node:events"); const ev = new EventEmitter(); const sock = { ev, ws: { close: vi.fn() }, sendPresenceUpdate: vi.fn().mockResolvedValue(undefined), sendMessage: vi.fn().mockResolvedValue(undefined), readMessages: vi.fn().mockResolvedValue(undefined), updateMediaMessage: vi.fn(), logger: {}, signalRepository: { lidMapping: { getPNForLID: vi.fn().mockResolvedValue(null), }, }, user: { id: "123@s.whatsapp.net" }, }; return { createWaSocket: vi.fn().mockResolvedValue(sock), waitForWaConnection: vi.fn().mockResolvedValue(undefined), getStatusCode: vi.fn(() => 500), }; }); const { createWaSocket } = await import("./session.js"); const _getSock = () => (createWaSocket as unknown as () => Promise>)(); import fsSync from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { resetLogger, setLoggerOverride } from "../logging.js"; import { monitorWebInbox, resetWebInboundDedupe } from "./inbound.js"; const ACCOUNT_ID = "default"; let authDir: string; describe("web monitor inbox", () => { beforeEach(() => { vi.clearAllMocks(); readAllowFromStoreMock.mockResolvedValue([]); upsertPairingRequestMock.mockResolvedValue({ code: "PAIRCODE", created: true, }); resetWebInboundDedupe(); authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "clawdbot-auth-")); }); afterEach(() => { resetLogger(); setLoggerOverride(null); vi.useRealTimers(); fsSync.rmSync(authDir, { recursive: true, force: true }); }); it("streams inbound messages", async () => { const onMessage = vi.fn(async (msg) => { await msg.sendComposing(); await msg.reply("pong"); }); const listener = await monitorWebInbox({ verbose: false, onMessage, accountId: ACCOUNT_ID, authDir, }); const sock = await createWaSocket(); expect(sock.sendPresenceUpdate).toHaveBeenCalledWith("available"); const upsert = { type: "notify", messages: [ { key: { id: "abc", fromMe: false, remoteJid: "999@s.whatsapp.net" }, message: { conversation: "ping" }, messageTimestamp: 1_700_000_000, pushName: "Tester", }, ], }; sock.ev.emit("messages.upsert", upsert); await new Promise((resolve) => setImmediate(resolve)); expect(onMessage).toHaveBeenCalledWith( expect.objectContaining({ body: "ping", from: "+999", to: "+123" }), ); expect(sock.readMessages).toHaveBeenCalledWith([ { remoteJid: "999@s.whatsapp.net", id: "abc", participant: undefined, fromMe: false, }, ]); expect(sock.sendPresenceUpdate).toHaveBeenCalledWith("available"); expect(sock.sendPresenceUpdate).toHaveBeenCalledWith("composing", "999@s.whatsapp.net"); expect(sock.sendMessage).toHaveBeenCalledWith("999@s.whatsapp.net", { text: "pong", }); await listener.close(); }); it("deduplicates redelivered messages by id", async () => { const onMessage = vi.fn(async () => { return; }); const listener = await monitorWebInbox({ verbose: false, onMessage, accountId: ACCOUNT_ID, authDir, }); const sock = await createWaSocket(); const upsert = { type: "notify", messages: [ { key: { id: "abc", fromMe: false, remoteJid: "999@s.whatsapp.net" }, message: { conversation: "ping" }, messageTimestamp: 1_700_000_000, pushName: "Tester", }, ], }; sock.ev.emit("messages.upsert", upsert); sock.ev.emit("messages.upsert", upsert); await new Promise((resolve) => setImmediate(resolve)); expect(onMessage).toHaveBeenCalledTimes(1); await listener.close(); }); it("resolves LID JIDs using Baileys LID mapping store", async () => { const onMessage = vi.fn(async () => { return; }); const listener = await monitorWebInbox({ verbose: false, onMessage, accountId: ACCOUNT_ID, authDir, }); const sock = await createWaSocket(); const getPNForLID = vi.spyOn(sock.signalRepository.lidMapping, "getPNForLID"); sock.signalRepository.lidMapping.getPNForLID.mockResolvedValueOnce("999:0@s.whatsapp.net"); const upsert = { type: "notify", messages: [ { key: { id: "abc", fromMe: false, remoteJid: "999@lid" }, message: { conversation: "ping" }, messageTimestamp: 1_700_000_000, pushName: "Tester", }, ], }; sock.ev.emit("messages.upsert", upsert); await new Promise((resolve) => setImmediate(resolve)); expect(getPNForLID).toHaveBeenCalledWith("999@lid"); expect(onMessage).toHaveBeenCalledWith( expect.objectContaining({ body: "ping", from: "+999", to: "+123" }), ); await listener.close(); }); it("resolves LID JIDs via authDir mapping files", async () => { const onMessage = vi.fn(async () => { return; }); fsSync.writeFileSync( path.join(authDir, "lid-mapping-555_reverse.json"), JSON.stringify("1555"), ); const listener = await monitorWebInbox({ verbose: false, onMessage, accountId: ACCOUNT_ID, authDir, }); const sock = await createWaSocket(); const getPNForLID = vi.spyOn(sock.signalRepository.lidMapping, "getPNForLID"); const upsert = { type: "notify", messages: [ { key: { id: "abc", fromMe: false, remoteJid: "555@lid" }, message: { conversation: "ping" }, messageTimestamp: 1_700_000_000, pushName: "Tester", }, ], }; sock.ev.emit("messages.upsert", upsert); await new Promise((resolve) => setImmediate(resolve)); expect(onMessage).toHaveBeenCalledWith( expect.objectContaining({ body: "ping", from: "+1555", to: "+123" }), ); expect(getPNForLID).not.toHaveBeenCalled(); await listener.close(); }); it("resolves group participant LID JIDs via Baileys mapping", async () => { const onMessage = vi.fn(async () => { return; }); const listener = await monitorWebInbox({ verbose: false, onMessage, accountId: ACCOUNT_ID, authDir, }); const sock = await createWaSocket(); const getPNForLID = vi.spyOn(sock.signalRepository.lidMapping, "getPNForLID"); sock.signalRepository.lidMapping.getPNForLID.mockResolvedValueOnce("444:0@s.whatsapp.net"); const upsert = { type: "notify", messages: [ { key: { id: "abc", fromMe: false, remoteJid: "123@g.us", participant: "444@lid", }, message: { conversation: "ping" }, messageTimestamp: 1_700_000_000, }, ], }; sock.ev.emit("messages.upsert", upsert); await new Promise((resolve) => setImmediate(resolve)); expect(getPNForLID).toHaveBeenCalledWith("444@lid"); expect(onMessage).toHaveBeenCalledWith( expect.objectContaining({ body: "ping", from: "123@g.us", senderE164: "+444", chatType: "group", }), ); await listener.close(); }); it("does not block follow-up messages when handler is pending", async () => { let resolveFirst: (() => void) | null = null; const onMessage = vi.fn(async () => { if (!resolveFirst) { await new Promise((resolve) => { resolveFirst = resolve; }); } }); const listener = await monitorWebInbox({ verbose: false, onMessage, accountId: ACCOUNT_ID, authDir, }); const sock = await createWaSocket(); const upsert = { type: "notify", messages: [ { key: { id: "abc1", fromMe: false, remoteJid: "999@s.whatsapp.net" }, message: { conversation: "ping" }, messageTimestamp: 1_700_000_000, }, { key: { id: "abc2", fromMe: false, remoteJid: "999@s.whatsapp.net" }, message: { conversation: "pong" }, messageTimestamp: 1_700_000_001, }, ], }; sock.ev.emit("messages.upsert", upsert); await new Promise((resolve) => setImmediate(resolve)); expect(onMessage).toHaveBeenCalledTimes(2); resolveFirst?.(); await listener.close(); }); it("captures reply context from quoted messages", async () => { const onMessage = vi.fn(async (msg) => { await msg.reply("pong"); }); const listener = await monitorWebInbox({ verbose: false, onMessage }); const sock = await createWaSocket(); const upsert = { type: "notify", messages: [ { key: { id: "abc", fromMe: false, remoteJid: "999@s.whatsapp.net" }, message: { extendedTextMessage: { text: "reply", contextInfo: { stanzaId: "q1", participant: "111@s.whatsapp.net", quotedMessage: { conversation: "original" }, }, }, }, messageTimestamp: 1_700_000_000, pushName: "Tester", }, ], }; sock.ev.emit("messages.upsert", upsert); await new Promise((resolve) => setImmediate(resolve)); expect(onMessage).toHaveBeenCalledWith( expect.objectContaining({ replyToId: "q1", replyToBody: "original", replyToSender: "+111", }), ); expect(sock.sendMessage).toHaveBeenCalledWith("999@s.whatsapp.net", { text: "pong", }); await listener.close(); }); it("captures reply context from wrapped quoted messages", async () => { const onMessage = vi.fn(async (msg) => { await msg.reply("pong"); }); const listener = await monitorWebInbox({ verbose: false, onMessage }); const sock = await createWaSocket(); const upsert = { type: "notify", messages: [ { key: { id: "abc", fromMe: false, remoteJid: "999@s.whatsapp.net" }, message: { extendedTextMessage: { text: "reply", contextInfo: { stanzaId: "q1", participant: "111@s.whatsapp.net", quotedMessage: { viewOnceMessageV2Extension: { message: { conversation: "original" }, }, }, }, }, }, messageTimestamp: 1_700_000_000, pushName: "Tester", }, ], }; sock.ev.emit("messages.upsert", upsert); await new Promise((resolve) => setImmediate(resolve)); expect(onMessage).toHaveBeenCalledWith( expect.objectContaining({ replyToId: "q1", replyToBody: "original", replyToSender: "+111", }), ); expect(sock.sendMessage).toHaveBeenCalledWith("999@s.whatsapp.net", { text: "pong", }); await listener.close(); }); });