From 8ff01b8bb32a6128e9510b4c44f49b02a34e8bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= <55464917+jlucaso1@users.noreply.github.com> Date: Tue, 20 Jan 2026 07:39:41 -0300 Subject: [PATCH] fix: store LID-PN mapping from contactAction sync (#2266) * fix: store LID-PN mapping from contactAction sync * chore: improve testing of sync actions --- src/Socket/chats.ts | 8 + src/Utils/chat-utils.ts | 13 +- src/Utils/sync-action-utils.ts | 74 ++++ .../Utils/process-sync-action.test.ts | 359 ++++++++++++++++++ src/__tests__/Utils/sync-action-utils.test.ts | 186 +++++++++ 5 files changed, 631 insertions(+), 9 deletions(-) create mode 100644 src/Utils/sync-action-utils.ts create mode 100644 src/__tests__/Utils/process-sync-action.test.ts create mode 100644 src/__tests__/Utils/sync-action-utils.test.ts diff --git a/src/Socket/chats.ts b/src/Socket/chats.ts index f7eae4ea..8e2de602 100644 --- a/src/Socket/chats.ts +++ b/src/Socket/chats.ts @@ -1185,6 +1185,14 @@ export const makeChatsSocket = (config: SocketConfig) => { }, 20_000) }) + ev.on('lid-mapping.update', async ({ lid, pn }) => { + try { + await signalRepository.lidMapping.storeLIDPNMappings([{ lid, pn }]) + } catch (error) { + logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping') + } + }) + return { ...sock, createCallLink, diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index f112777a..04cd8547 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -24,6 +24,7 @@ import { toNumber } from './generics' import type { ILogger } from './logger' import { LT_HASH_ANTI_TAMPERING } from './lt-hash' import { downloadContentFromMessage } from './messages-media' +import { emitSyncActionResults, processContactAction } from './sync-action-utils' type FetchAppStateSyncKey = (keyId: string) => Promise @@ -832,14 +833,8 @@ export const processSyncAction = ( ] }) } else if (action?.contactAction) { - ev.emit('contacts.upsert', [ - { - id: id!, - name: action.contactAction.fullName!, - lid: action.contactAction.lidJid || undefined, - phoneNumber: action.contactAction.pnJid || undefined - } - ]) + const results = processContactAction(action.contactAction, id, logger) + emitSyncActionResults(ev, results) } else if (action?.pushNameSetting) { const name = action?.pushNameSetting?.name if (name && me?.name !== name) { @@ -935,7 +930,7 @@ export const processSyncAction = ( ev.emit('contacts.upsert', [ { id: id!, - name: action.lidContactAction.fullName!, + name: action.lidContactAction.fullName || undefined, lid: id!, phoneNumber: undefined } diff --git a/src/Utils/sync-action-utils.ts b/src/Utils/sync-action-utils.ts new file mode 100644 index 00000000..d53b00fb --- /dev/null +++ b/src/Utils/sync-action-utils.ts @@ -0,0 +1,74 @@ +import { proto } from '../../WAProto/index.js' +import type { BaileysEventEmitter, BaileysEventMap, Contact } from '../Types' +import { isLidUser, isPnUser } from '../WABinary' +import type { ILogger } from './logger' + +export type ContactsUpsertResult = { + event: 'contacts.upsert' + data: Contact[] +} + +export type LidMappingUpdateResult = { + event: 'lid-mapping.update' + data: BaileysEventMap['lid-mapping.update'] +} + +export type SyncActionResult = ContactsUpsertResult | LidMappingUpdateResult + +/** + * Process contactAction and return events to emit. + * Pure function - no side effects. + */ +export const processContactAction = ( + action: proto.SyncActionValue.IContactAction, + id: string | undefined, + logger?: ILogger +): SyncActionResult[] => { + const results: SyncActionResult[] = [] + + if (!id) { + logger?.warn( + { hasFullName: !!action.fullName, hasLidJid: !!action.lidJid, hasPnJid: !!action.pnJid }, + 'contactAction sync: missing id in index' + ) + return results + } + + const lidJid = action.lidJid + const idIsPn = isPnUser(id) + // PN is in index[1], not in contactAction.pnJid which is usually null + const phoneNumber = idIsPn ? id : action.pnJid || undefined + + // Always emit contacts.upsert + results.push({ + event: 'contacts.upsert', + data: [ + { + id, + name: action.fullName || undefined, + lid: lidJid || undefined, + phoneNumber + } + ] + }) + + // Emit lid-mapping.update if we have valid LID-PN pair + if (lidJid && isLidUser(lidJid) && idIsPn) { + results.push({ + event: 'lid-mapping.update', + data: { lid: lidJid, pn: id } + }) + } + + return results +} + +export const emitSyncActionResults = (ev: BaileysEventEmitter, results: SyncActionResult[]): void => { + for (const result of results) { + if (result.event === 'contacts.upsert') { + ev.emit('contacts.upsert', result.data) + } else { + ev.emit('lid-mapping.update', result.data) + } + } +} diff --git a/src/__tests__/Utils/process-sync-action.test.ts b/src/__tests__/Utils/process-sync-action.test.ts new file mode 100644 index 00000000..2da93d30 --- /dev/null +++ b/src/__tests__/Utils/process-sync-action.test.ts @@ -0,0 +1,359 @@ +import { jest } from '@jest/globals' +import { proto } from '../../../WAProto/index.js' +import type { BaileysEventEmitter, ChatMutation, Contact } from '../../Types' +import { LabelAssociationType } from '../../Types/LabelAssociation' +import { processSyncAction } from '../../Utils/chat-utils' +import type { ILogger } from '../../Utils/logger' + +const createMockEventEmitter = () => { + const emittedEvents: Array<{ event: string; data: unknown }> = [] + const emit = jest.fn((event: string, data: unknown) => { + emittedEvents.push({ event, data }) + return true + }) + return { + emit, + emittedEvents, + on: jest.fn(), + off: jest.fn(), + removeAllListeners: jest.fn() + } as unknown as BaileysEventEmitter & { emittedEvents: typeof emittedEvents } +} + +const createMockLogger = (): ILogger => + ({ + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + trace: jest.fn(), + child: jest.fn(function (this: ILogger) { + return this + }), + level: 'silent' + }) as unknown as ILogger + +const createSyncAction = ( + action: proto.ISyncActionValue, + index: string[] = ['type', 'id', 'msgId', '0'] +): ChatMutation => ({ + syncAction: { value: action }, + index +}) + +const mockMe: Contact = { id: 'me@s.whatsapp.net', name: 'Test User' } + +describe('processSyncAction', () => { + let ev: ReturnType + let logger: ILogger + + beforeEach(() => { + jest.clearAllMocks() + ev = createMockEventEmitter() + logger = createMockLogger() + }) + + describe('muteAction', () => { + it('emits chats.update with muteEndTime when muted', () => { + const syncAction = createSyncAction({ muteAction: { muted: true, muteEndTimestamp: 1700000000 } }, [ + 'mute', + 'chat123@s.whatsapp.net' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith( + 'chats.update', + expect.arrayContaining([expect.objectContaining({ id: 'chat123@s.whatsapp.net', muteEndTime: 1700000000 })]) + ) + }) + + it('emits null muteEndTime when unmuted', () => { + const syncAction = createSyncAction({ muteAction: { muted: false, muteEndTimestamp: 0 } }, [ + 'mute', + 'chat123@s.whatsapp.net' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith( + 'chats.update', + expect.arrayContaining([expect.objectContaining({ muteEndTime: null })]) + ) + }) + }) + + describe('archiveChatAction', () => { + it('emits chats.update with archived true/false', () => { + const archived = createSyncAction({ archiveChatAction: { archived: true } }, ['archive', 'chat@s.whatsapp.net']) + processSyncAction(archived, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith( + 'chats.update', + expect.arrayContaining([expect.objectContaining({ archived: true })]) + ) + }) + + it('handles type fallback without archiveChatAction', () => { + const syncAction = createSyncAction({}, ['archive', 'chat@s.whatsapp.net']) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith( + 'chats.update', + expect.arrayContaining([expect.objectContaining({ archived: true })]) + ) + }) + }) + + describe('markChatAsReadAction', () => { + it('emits unreadCount 0 when read is true', () => { + const read = createSyncAction({ markChatAsReadAction: { read: true } }, ['markRead', 'chat@s.whatsapp.net']) + processSyncAction(read, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith( + 'chats.update', + expect.arrayContaining([expect.objectContaining({ unreadCount: 0 })]) + ) + }) + + it('emits unreadCount -1 when read is false', () => { + const unread = createSyncAction({ markChatAsReadAction: { read: false } }, ['markRead', 'chat@s.whatsapp.net']) + processSyncAction(unread, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith( + 'chats.update', + expect.arrayContaining([expect.objectContaining({ unreadCount: -1 })]) + ) + }) + + it('emits null unreadCount during initial sync when already read', () => { + const syncAction = createSyncAction({ markChatAsReadAction: { read: true } }, ['markRead', 'chat@s.whatsapp.net']) + processSyncAction(syncAction, ev, mockMe, { accountSettings: { unarchiveChats: false } }, logger) + expect(ev.emit).toHaveBeenCalledWith( + 'chats.update', + expect.arrayContaining([expect.objectContaining({ unreadCount: null })]) + ) + }) + }) + + describe('deleteMessageForMeAction', () => { + it('emits messages.delete with correct key', () => { + const syncAction = createSyncAction({ deleteMessageForMeAction: { deleteMedia: false } }, [ + 'deleteMessageForMe', + 'chat@s.whatsapp.net', + 'msg456', + '1' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('messages.delete', { + keys: [{ remoteJid: 'chat@s.whatsapp.net', id: 'msg456', fromMe: true }] + }) + }) + }) + + describe('contactAction', () => { + it('emits contacts.upsert and lid-mapping.update for PN user with LID', () => { + const syncAction = createSyncAction({ contactAction: { fullName: 'John', lidJid: '123@lid', pnJid: null } }, [ + 'contact', + '5511999@s.whatsapp.net' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('contacts.upsert', [ + { + id: '5511999@s.whatsapp.net', + name: 'John', + lid: '123@lid', + phoneNumber: '5511999@s.whatsapp.net' + } + ]) + expect(ev.emit).toHaveBeenCalledWith('lid-mapping.update', { + lid: '123@lid', + pn: '5511999@s.whatsapp.net' + }) + }) + + it('does not emit events when id is missing', () => { + const syncAction = createSyncAction({ contactAction: { fullName: 'John', lidJid: '123@lid', pnJid: null } }, [ + 'contact', + '' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emittedEvents.filter(e => e.event === 'contacts.upsert')).toHaveLength(0) + }) + }) + + describe('pushNameSetting', () => { + it('emits creds.update when name differs', () => { + const syncAction = createSyncAction({ pushNameSetting: { name: 'New' } }, ['pushName']) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('creds.update', { me: { ...mockMe, name: 'New' } }) + }) + + it('does not emit when name is same or empty', () => { + const same = createSyncAction({ pushNameSetting: { name: 'Test User' } }, ['pushName']) + processSyncAction(same, ev, mockMe, undefined, logger) + expect(ev.emit).not.toHaveBeenCalled() + }) + }) + + describe('pinAction', () => { + it('emits chats.update with pinned timestamp or null', () => { + const syncAction: ChatMutation = { + syncAction: { value: { pinAction: { pinned: true }, timestamp: 1700000000 } }, + index: ['pin', 'chat@s.whatsapp.net'] + } + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith( + 'chats.update', + expect.arrayContaining([expect.objectContaining({ pinned: 1700000000 })]) + ) + }) + }) + + describe('starAction', () => { + it('emits messages.update with starred value', () => { + const syncAction = createSyncAction({ starAction: { starred: true } }, [ + 'star', + 'chat@s.whatsapp.net', + 'msg', + '1' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('messages.update', [ + { + key: { remoteJid: 'chat@s.whatsapp.net', id: 'msg', fromMe: true }, + update: { starred: true } + } + ]) + }) + }) + + describe('deleteChatAction', () => { + it('emits chats.delete when not initial sync', () => { + const syncAction = createSyncAction({ deleteChatAction: { messageRange: null } }, [ + 'deleteChat', + 'chat@s.whatsapp.net' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('chats.delete', ['chat@s.whatsapp.net']) + }) + + it('does NOT emit during initial sync', () => { + const syncAction = createSyncAction({ deleteChatAction: { messageRange: null } }, [ + 'deleteChat', + 'chat@s.whatsapp.net' + ]) + processSyncAction(syncAction, ev, mockMe, { accountSettings: { unarchiveChats: false } }, logger) + expect(ev.emit).not.toHaveBeenCalled() + }) + }) + + describe('labelEditAction', () => { + it('emits labels.edit', () => { + const syncAction = createSyncAction( + { labelEditAction: { name: 'Important', color: 1, deleted: false, predefinedId: 5 } }, + ['label', 'label123'] + ) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('labels.edit', { + id: 'label123', + name: 'Important', + color: 1, + deleted: false, + predefinedId: '5' + }) + }) + }) + + describe('labelAssociationAction', () => { + it('emits labels.association for chat label', () => { + const syncAction = createSyncAction({ labelAssociationAction: { labeled: true } }, [ + LabelAssociationType.Chat, + 'label123', + 'chat@s.whatsapp.net' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('labels.association', { + type: 'add', + association: { type: LabelAssociationType.Chat, chatId: 'chat@s.whatsapp.net', labelId: 'label123' } + }) + }) + + it('emits labels.association for message label', () => { + const syncAction = createSyncAction({ labelAssociationAction: { labeled: true } }, [ + LabelAssociationType.Message, + 'label123', + 'chat@s.whatsapp.net', + 'msg789' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('labels.association', { + type: 'add', + association: { + type: LabelAssociationType.Message, + chatId: 'chat@s.whatsapp.net', + messageId: 'msg789', + labelId: 'label123' + } + }) + }) + }) + + describe('pnForLidChatAction', () => { + it('emits lid-mapping.update when pnJid is present', () => { + const syncAction = createSyncAction({ pnForLidChatAction: { pnJid: '5511999@s.whatsapp.net' } }, [ + 'pnForLid', + '123@lid' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('lid-mapping.update', { lid: '123@lid', pn: '5511999@s.whatsapp.net' }) + }) + + it('does not emit when pnJid is missing', () => { + const syncAction = createSyncAction({ pnForLidChatAction: { pnJid: '' } }, ['pnForLid', '123@lid']) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).not.toHaveBeenCalled() + }) + }) + + describe('lockChatAction', () => { + it('emits chats.lock', () => { + const syncAction = createSyncAction({ lockChatAction: { locked: true } }, ['lockChat', 'chat@s.whatsapp.net']) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('chats.lock', { id: 'chat@s.whatsapp.net', locked: true }) + }) + }) + + describe('lidContactAction', () => { + it('emits contacts.upsert with LID contact', () => { + const syncAction = createSyncAction({ lidContactAction: { fullName: 'LID Contact' } }, ['lidContact', '123@lid']) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('contacts.upsert', [ + { + id: '123@lid', + name: 'LID Contact', + lid: '123@lid', + phoneNumber: undefined + } + ]) + }) + }) + + describe('settings actions', () => { + it('localeSetting emits settings.update', () => { + const syncAction = createSyncAction({ localeSetting: { locale: 'en-US' } }, ['locale']) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('settings.update', { setting: 'locale', value: 'en-US' }) + }) + + it('unarchiveChatsSetting emits creds.update', () => { + const syncAction = createSyncAction({ unarchiveChatsSetting: { unarchiveChats: true } }, ['unarchiveChats']) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(ev.emit).toHaveBeenCalledWith('creds.update', { accountSettings: { unarchiveChats: true } }) + }) + }) + + describe('unprocessable actions', () => { + it('logs debug for unknown action', () => { + const syncAction = createSyncAction({ unknownAction: {} } as unknown as proto.ISyncActionValue, [ + 'unknown', + 'id123' + ]) + processSyncAction(syncAction, ev, mockMe, undefined, logger) + expect(logger.debug).toHaveBeenCalledWith({ syncAction, id: 'id123' }, 'unprocessable update') + expect(ev.emit).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/__tests__/Utils/sync-action-utils.test.ts b/src/__tests__/Utils/sync-action-utils.test.ts new file mode 100644 index 00000000..cd608c3d --- /dev/null +++ b/src/__tests__/Utils/sync-action-utils.test.ts @@ -0,0 +1,186 @@ +import { jest } from '@jest/globals' +import type { BaileysEventMap, Contact } from '../../Types' +import type { ILogger } from '../../Utils/logger' +import { processContactAction } from '../../Utils/sync-action-utils' + +type LidMapping = BaileysEventMap['lid-mapping.update'] + +describe('processContactAction', () => { + const mockLogger: ILogger = { + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + trace: jest.fn(), + child: jest.fn(() => mockLogger), + level: 'silent' + } as unknown as ILogger + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('contacts.upsert', () => { + it('emits with phoneNumber from id when id is PN user', () => { + const action = { fullName: 'John Doe', lidJid: '123456789@lid', pnJid: null } + const id = '5511999999999@s.whatsapp.net' + + const results = processContactAction(action, id) + + expect(results).toContainEqual({ + event: 'contacts.upsert', + data: [ + { + id: '5511999999999@s.whatsapp.net', + name: 'John Doe', + lid: '123456789@lid', + phoneNumber: '5511999999999@s.whatsapp.net' + } + ] + }) + }) + + it('uses pnJid as phoneNumber fallback when id is LID user', () => { + const action = { fullName: 'John Doe', lidJid: null, pnJid: '5511888888888@s.whatsapp.net' } + const id = '123456789@lid' + + const results = processContactAction(action, id) + + expect(results).toContainEqual({ + event: 'contacts.upsert', + data: [ + { + id: '123456789@lid', + name: 'John Doe', + lid: undefined, + phoneNumber: '5511888888888@s.whatsapp.net' + } + ] + }) + }) + + it('handles undefined fullName', () => { + const action = { fullName: undefined, lidJid: '123456789@lid', pnJid: null } + const id = '5511999999999@s.whatsapp.net' + + const results = processContactAction(action, id) + + const contactData = results.find(r => r.event === 'contacts.upsert')!.data + expect(contactData[0]!.name).toBeUndefined() + }) + }) + + describe('lid-mapping.update', () => { + it('emits when LID and PN are valid', () => { + const action = { fullName: 'John Doe', lidJid: '123456789@lid', pnJid: null } + const id = '5511999999999@s.whatsapp.net' + + const results = processContactAction(action, id) + + expect(results).toContainEqual({ + event: 'lid-mapping.update', + data: { lid: '123456789@lid', pn: '5511999999999@s.whatsapp.net' } + }) + }) + + it('handles LID with device ID suffix', () => { + const action = { fullName: 'Contact', lidJid: '173233882013816:99@lid', pnJid: null } + const id = '5511999999999@s.whatsapp.net' + + const results = processContactAction(action, id) + + expect(results).toContainEqual({ + event: 'lid-mapping.update', + data: { lid: '173233882013816:99@lid', pn: '5511999999999@s.whatsapp.net' } + }) + }) + + it('does NOT emit when lidJid is missing', () => { + const action = { fullName: 'John Doe', lidJid: null, pnJid: null } + const id = '5511999999999@s.whatsapp.net' + + const results = processContactAction(action, id) + + expect(results.find(r => r.event === 'lid-mapping.update')).toBeUndefined() + }) + + it('does NOT emit when id is LID user (not PN)', () => { + const action = { fullName: 'John Doe', lidJid: '123456789@lid', pnJid: null } + const id = '987654321@lid' + + const results = processContactAction(action, id) + + expect(results.find(r => r.event === 'lid-mapping.update')).toBeUndefined() + }) + + it('does NOT emit when lidJid is invalid format', () => { + const action = { fullName: 'John Doe', lidJid: 'invalid-lid-format', pnJid: null } + const id = '5511999999999@s.whatsapp.net' + + const results = processContactAction(action, id) + + expect(results.find(r => r.event === 'lid-mapping.update')).toBeUndefined() + }) + + it('does NOT emit for group JIDs', () => { + const action = { fullName: 'Group', lidJid: '123456789@lid', pnJid: null } + const id = '123456789012345678@g.us' + + const results = processContactAction(action, id) + + expect(results.find(r => r.event === 'lid-mapping.update')).toBeUndefined() + }) + }) + + describe('missing id', () => { + it('returns empty array and logs warning when id is undefined', () => { + const action = { fullName: 'John Doe', lidJid: '123456789@lid', pnJid: null } + + const results = processContactAction(action, undefined, mockLogger) + + expect(results).toEqual([]) + expect(mockLogger.warn).toHaveBeenCalledWith( + { hasFullName: true, hasLidJid: true, hasPnJid: false }, + 'contactAction sync: missing id in index' + ) + }) + + it('returns empty array when id is empty string', () => { + const action = { fullName: 'John Doe', lidJid: '123456789@lid', pnJid: null } + + const results = processContactAction(action, '', mockLogger) + + expect(results).toEqual([]) + }) + }) + + describe('PN extraction from index[1] (pnJid is always null)', () => { + it('extracts PN from id when pnJid is null', () => { + // In real WhatsApp data, pnJid is ALWAYS null - PN comes from index[1] + const action = { fullName: 'Test Contact', lidJid: '111222333@lid', pnJid: null } + const id = '5599887766@s.whatsapp.net' + + const results = processContactAction(action, id) + + const contactData = results.find(r => r.event === 'contacts.upsert')!.data + expect(contactData[0]!.phoneNumber).toBe('5599887766@s.whatsapp.net') + expect(contactData[0]!.lid).toBe('111222333@lid') + + const mapping = results.find(r => r.event === 'lid-mapping.update')!.data + expect(mapping).toEqual({ lid: '111222333@lid', pn: '5599887766@s.whatsapp.net' }) + }) + + it('prefers id over pnJid when id is PN user', () => { + const action = { fullName: 'Test', lidJid: '111222333@lid', pnJid: '1111111111@s.whatsapp.net' } + const id = '9999999999@s.whatsapp.net' + + const results = processContactAction(action, id) + + const contactData = results.find(r => r.event === 'contacts.upsert')!.data + expect(contactData[0]!.phoneNumber).toBe('9999999999@s.whatsapp.net') + + const mapping = results.find(r => r.event === 'lid-mapping.update')!.data + expect(mapping.pn).toBe('9999999999@s.whatsapp.net') + }) + }) +})