diff --git a/src/web/auto-reply.whatsapp-inbound-reaction-system-events.test.ts b/src/web/auto-reply.whatsapp-inbound-reaction-system-events.test.ts index d648855c9..8145e3e1d 100644 --- a/src/web/auto-reply.whatsapp-inbound-reaction-system-events.test.ts +++ b/src/web/auto-reply.whatsapp-inbound-reaction-system-events.test.ts @@ -184,4 +184,63 @@ describe("web auto-reply – inbound reaction system events", () => { expect(events).toHaveLength(1); expect(events[0]).toBe("WhatsApp reaction added: 🔥 by someone msg msg-noid"); }); + + it("normalizes DM reaction peer ID to E.164 matching message routing", async () => { + setLoadConfigMock(() => ({ + channels: { whatsapp: { allowFrom: ["*"] } }, + messages: {}, + })); + + let capturedOnReaction: ((reaction: WebInboundReaction) => void) | undefined; + const listenerFactory = async (opts: { + onMessage: (...args: unknown[]) => Promise; + onReaction?: (reaction: WebInboundReaction) => void; + }) => { + capturedOnReaction = opts.onReaction; + return { close: vi.fn() }; + }; + + await monitorWebChannel(false, listenerFactory, false); + + const cfg = { channels: { whatsapp: { allowFrom: ["*"] } }, messages: {} }; + + // For DM reactions with senderE164, the peer ID should be normalized to E.164 + // to match how messages are routed (via resolvePeerId). + const normalizedRoute = resolveAgentRoute({ + cfg: cfg as Parameters[0]["cfg"], + channel: "whatsapp", + accountId: "default", + peer: { kind: "dm", id: "+19995551234" }, + }); + + // Drain both potential session keys to clear "gateway connected" events + drainSystemEvents(normalizedRoute.sessionKey); + const jidRoute = resolveAgentRoute({ + cfg: cfg as Parameters[0]["cfg"], + channel: "whatsapp", + accountId: "default", + peer: { kind: "dm", id: "19995551234@s.whatsapp.net" }, + }); + drainSystemEvents(jidRoute.sessionKey); + + capturedOnReaction!({ + messageId: "msg-normalized", + emoji: "✅", + chatJid: "19995551234@s.whatsapp.net", + chatType: "direct", + accountId: "default", + senderJid: "19995551234@s.whatsapp.net", + senderE164: "+19995551234", + timestamp: Date.now(), + }); + + // The reaction should land in the E.164-normalized session, not the JID-based one. + const events = peekSystemEvents(normalizedRoute.sessionKey); + expect(events).toHaveLength(1); + expect(events[0]).toBe("WhatsApp reaction added: ✅ by +19995551234 msg msg-normalized"); + + // Verify it did NOT land in a JID-based session key. + const jidEvents = peekSystemEvents(jidRoute.sessionKey); + expect(jidEvents).toHaveLength(0); + }); }); diff --git a/src/web/auto-reply/monitor.ts b/src/web/auto-reply/monitor.ts index ee02bc01d..2b5ab2315 100644 --- a/src/web/auto-reply/monitor.ts +++ b/src/web/auto-reply/monitor.ts @@ -28,6 +28,7 @@ import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js"; import { buildMentionConfig } from "./mentions.js"; import { createEchoTracker } from "./monitor/echo.js"; import { createWebOnMessageHandler } from "./monitor/on-message.js"; +import { resolveReactionPeerId } from "./monitor/peer.js"; import type { WebInboundReaction } from "../inbound.js"; import type { WebChannelStatus, WebInboundMsg, WebMonitorTuning } from "./types.js"; import { isLikelyWhatsAppCryptoError } from "./util.js"; @@ -205,13 +206,14 @@ export async function monitorWebChannel( onReaction: (reaction: WebInboundReaction) => { status.lastEventAt = Date.now(); emitStatus(); + const peerId = resolveReactionPeerId(reaction); const route = resolveAgentRoute({ cfg, channel: "whatsapp", accountId: reaction.accountId, peer: { kind: reaction.chatType === "group" ? "group" : "dm", - id: reaction.chatJid, + id: peerId, }, }); const senderLabel = reaction.senderE164 ?? reaction.senderJid ?? "someone"; diff --git a/src/web/auto-reply/monitor/peer.ts b/src/web/auto-reply/monitor/peer.ts index 0ebd8ac66..25ececb11 100644 --- a/src/web/auto-reply/monitor/peer.ts +++ b/src/web/auto-reply/monitor/peer.ts @@ -1,5 +1,6 @@ import { jidToE164, normalizeE164 } from "../../../utils.js"; import type { WebInboundMsg } from "../types.js"; +import type { WebInboundReaction } from "../../inbound/types.js"; export function resolvePeerId(msg: WebInboundMsg) { if (msg.chatType === "group") return msg.conversationId ?? msg.from; @@ -7,3 +8,10 @@ export function resolvePeerId(msg: WebInboundMsg) { if (msg.from.includes("@")) return jidToE164(msg.from) ?? msg.from; return normalizeE164(msg.from) ?? msg.from; } + +export function resolveReactionPeerId(reaction: WebInboundReaction) { + if (reaction.chatType === "group") return reaction.chatJid; + if (reaction.senderE164) return normalizeE164(reaction.senderE164) ?? reaction.senderE164; + if (reaction.chatJid.includes("@")) return jidToE164(reaction.chatJid) ?? reaction.chatJid; + return normalizeE164(reaction.chatJid) ?? reaction.chatJid; +}