merge: resolve conflict with channels branch in whatsapp.test.ts

Combine both test suites — inbound bot mention detection (#2560 from
channels) and outbound parseWhatsAppMentions (this PR).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-05-22 23:16:48 +03:00
2 changed files with 177 additions and 3 deletions
+99 -1
View File
@@ -1,6 +1,104 @@
/**
* Regression coverage for #2560 — group @-mentions of the bot must set
* `InboundMessage.isMention`. Before the fix, the inbound construction
* site hard-coded `isMention: !isGroup ? true : undefined`, which dropped
* every group mention on the floor and prevented the router from waking
* the agent on a mention-only trigger.
*
* The detection logic lives in the exported pure helper `isBotMentionedInGroup`;
* the inbound site calls it with `normalized`, `botPhoneJid`, `botLidUser`.
* `isMention` is then computed as:
*
* isMention: !isGroup ? true : botMentionedInGroup ? true : undefined
*
* Both the helper and the call-site ternary are covered below so a future
* refactor that breaks either part fails this suite.
*/
import { describe, it, expect } from 'vitest';
import { parseWhatsAppMentions } from './whatsapp.js';
import { computeIsMention, isBotMentionedInGroup, parseWhatsAppMentions } from './whatsapp.js';
const BOT_PHONE_JID = '15550009999@s.whatsapp.net';
const BOT_LID_USER = '987654321';
describe('isBotMentionedInGroup (#2560)', () => {
it('detects the bot phone JID in extendedTextMessage.contextInfo.mentionedJid', () => {
const normalized = {
extendedTextMessage: {
text: 'hey @15550009999 take a look',
contextInfo: { mentionedJid: [BOT_PHONE_JID] },
},
};
expect(isBotMentionedInGroup(normalized, BOT_PHONE_JID, BOT_LID_USER)).toBe(true);
});
it('returns false when the bot is not in mentionedJid', () => {
const normalized = {
extendedTextMessage: {
text: 'hey @15551112222 take a look',
contextInfo: { mentionedJid: ['15551112222@s.whatsapp.net'] },
},
};
expect(isBotMentionedInGroup(normalized, BOT_PHONE_JID, BOT_LID_USER)).toBe(false);
});
it('detects an LID-only mention when no phone JID is in the list', () => {
// Modern WhatsApp clients increasingly emit the LID even when the
// human typed a phone-number mention; the phone JID may not appear.
const normalized = {
extendedTextMessage: {
contextInfo: { mentionedJid: [`${BOT_LID_USER}@lid`] },
},
};
expect(isBotMentionedInGroup(normalized, BOT_PHONE_JID, BOT_LID_USER)).toBe(true);
});
it('detects a mention in an image caption', () => {
const normalized = {
imageMessage: {
caption: 'check this @15550009999',
contextInfo: { mentionedJid: [BOT_PHONE_JID] },
},
};
expect(isBotMentionedInGroup(normalized, BOT_PHONE_JID, BOT_LID_USER)).toBe(true);
});
it('returns false on an empty / missing mentionedJid array', () => {
expect(isBotMentionedInGroup({}, BOT_PHONE_JID, BOT_LID_USER)).toBe(false);
expect(
isBotMentionedInGroup(
{ extendedTextMessage: { contextInfo: { mentionedJid: [] } } },
BOT_PHONE_JID,
BOT_LID_USER,
),
).toBe(false);
});
it('returns false when neither bot identifier is known', () => {
const normalized = {
extendedTextMessage: {
contextInfo: { mentionedJid: [BOT_PHONE_JID, `${BOT_LID_USER}@lid`] },
},
};
expect(isBotMentionedInGroup(normalized, undefined, undefined)).toBe(false);
});
});
describe('InboundMessage.isMention semantics (#2560)', () => {
it('is undefined for a group message with no bot mention', () => {
expect(computeIsMention(true, false)).toBeUndefined();
});
it('is true for a group message where the bot is mentioned', () => {
expect(computeIsMention(true, true)).toBe(true);
});
it('is true for a DM regardless of mention state', () => {
// DMs are unconditionally mentions — the helper isn't consulted there.
expect(computeIsMention(false, false)).toBe(true);
expect(computeIsMention(false, true)).toBe(true);
});
});
describe('parseWhatsAppMentions', () => {
it('returns empty mentions for plain text', () => {
+78 -2
View File
@@ -188,6 +188,64 @@ function formatWhatsApp(text: string): { text: string; mentions: string[] } {
return { text: out, mentions: [...mentions] };
}
/**
* Subset of a normalized Baileys message content carrying the message
* types that can host a `contextInfo.mentionedJid` array. Kept as a
* structural type so the helper (and its tests) don't pull in the full
* `proto.IMessage` shape just to construct fixtures.
*/
type MentionContextSource = {
extendedTextMessage?: { contextInfo?: { mentionedJid?: string[] | null } | null } | null;
imageMessage?: { contextInfo?: { mentionedJid?: string[] | null } | null } | null;
videoMessage?: { contextInfo?: { mentionedJid?: string[] | null } | null } | null;
documentMessage?: { contextInfo?: { mentionedJid?: string[] | null } | null } | null;
};
/**
* Detect an explicit @-mention of the bot in a WhatsApp group message.
* WhatsApp carries mentions in `contextInfo.mentionedJid` on the text +
* caption-bearing message types. Matches against both the bot's phone
* JID and LID — most modern clients emit the LID even when the human
* typed a phone-number mention.
*
* Exported for unit testing. The inbound construction site calls this
* to set `InboundMessage.isMention` for group messages (#2560). DMs are
* unconditionally mentions and don't go through this helper.
*/
export function isBotMentionedInGroup(
normalized: MentionContextSource,
botPhoneJid: string | undefined,
botLidUser: string | undefined,
): boolean {
if (!botPhoneJid && !botLidUser) return false;
const mentionedJids: string[] = [
...(normalized.extendedTextMessage?.contextInfo?.mentionedJid ?? []),
...(normalized.imageMessage?.contextInfo?.mentionedJid ?? []),
...(normalized.videoMessage?.contextInfo?.mentionedJid ?? []),
...(normalized.documentMessage?.contextInfo?.mentionedJid ?? []),
];
const botLidJid = botLidUser ? `${botLidUser}@lid` : undefined;
return mentionedJids.some((jid) => {
if (!jid) return false;
const bare = jid.split(':')[0];
return bare === botPhoneJid || bare === botLidJid;
});
}
/**
* Compute `InboundMessage.isMention` for a WhatsApp message:
* - DMs are always mentions (router auto-engages on the bot's behalf).
* - Group messages are mentions only when the bot is explicitly tagged.
*
* Returns `true | undefined` rather than `true | false` because the
* `InboundMessage` field is `isMention?: boolean` and downstream code
* treats `undefined` differently than an explicit `false` (#2560).
*/
export function computeIsMention(isGroup: boolean, botMentionedInGroup: boolean): true | undefined {
if (!isGroup) return true;
return botMentionedInGroup ? true : undefined;
}
/** Map file extension to Baileys media message type. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function buildMediaMessage(data: Buffer, filename: string, ext: string, caption?: string): any {
@@ -511,6 +569,17 @@ registerChannelAdapter('whatsapp', {
});
} else {
log.info('WhatsApp logged out');
// Delete auth credentials immediately. Keeping stale credentials
// causes the next service restart to attempt authentication with an
// invalidated session, producing a second 401 that can trigger
// WhatsApp's re-link cooldown ("can't link new devices now").
try {
fs.rmSync(authDir, { recursive: true, force: true });
fs.mkdirSync(authDir, { recursive: true });
log.info('WhatsApp auth cleared — set WHATSAPP_ENABLED=true and restart to re-link');
} catch (err) {
log.error('Failed to clear WhatsApp auth after logout', { err });
}
if (rejectFirstOpen) {
rejectFirstOpen(new Error('WhatsApp logged out'));
rejectFirstOpen = undefined;
@@ -653,14 +722,21 @@ registerChannelAdapter('whatsapp', {
}
}
// Detect explicit @-mentions of the bot in groups. Detail in
// isBotMentionedInGroup(); short version is contextInfo.mentionedJid
// on text + caption-bearing messages, matched against the bot's
// phone JID and LID (#2560).
const botMentionedInGroup = isGroup && isBotMentionedInGroup(normalized, botPhoneJid, botLidUser);
const inbound: InboundMessage = {
id: msg.key.id || `wa-${Date.now()}`,
kind: 'chat',
// DMs are addressed to the bot by definition. Mark them as
// platform-confirmed mentions so the router auto-creates an
// approval-required messaging_group when the chat is unknown,
// instead of silently dropping.
isMention: !isGroup ? true : undefined,
// instead of silently dropping. In groups, only an explicit
// @-mention counts.
isMention: computeIsMention(isGroup, botMentionedInGroup),
isGroup,
content: {
text: content,