feat: add identity change handler for improved connection stability
feat: add identity change handler for improved connection stability
This commit is contained in:
+13
-16
@@ -36,6 +36,7 @@ import {
|
||||
getHistoryMsg,
|
||||
getNextPreKeys,
|
||||
getStatusFromReceiptType,
|
||||
handleIdentityChange,
|
||||
hkdf,
|
||||
MISSING_KEYS_ERROR_TEXT,
|
||||
NACK_REASONS,
|
||||
@@ -564,21 +565,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
await uploadPreKeys()
|
||||
}
|
||||
} else {
|
||||
const identityNode = getBinaryNodeChild(node, 'identity')
|
||||
if (identityNode) {
|
||||
logger.info({ jid: from }, 'identity changed')
|
||||
if (identityAssertDebounce.get(from!)) {
|
||||
logger.debug({ jid: from }, 'skipping identity assert (debounced)')
|
||||
return
|
||||
}
|
||||
const result = await handleIdentityChange(node, {
|
||||
meId: authState.creds.me?.id,
|
||||
meLid: authState.creds.me?.lid,
|
||||
validateSession: signalRepository.validateSession,
|
||||
assertSessions,
|
||||
debounceCache: identityAssertDebounce,
|
||||
logger
|
||||
})
|
||||
|
||||
identityAssertDebounce.set(from!, true)
|
||||
try {
|
||||
await assertSessions([from!], true)
|
||||
} catch (error) {
|
||||
logger.warn({ error, jid: from }, 'failed to assert sessions after identity change')
|
||||
}
|
||||
} else {
|
||||
if (result.action === 'no_identity_node') {
|
||||
logger.info({ node }, 'unknown encrypt notification')
|
||||
}
|
||||
}
|
||||
@@ -1239,9 +1235,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
await decrypt()
|
||||
// message failed to decrypt
|
||||
if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
|
||||
// Handle "Missing keys" - standard decryption failure, just ACK
|
||||
// Handle "Missing keys" - standard decryption failure
|
||||
// Return NACK with parsing error to signal the issue
|
||||
if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
|
||||
return sendMessageAck(node)
|
||||
return sendMessageAck(node, NACK_REASONS.ParsingError)
|
||||
}
|
||||
|
||||
// Handle "Message absent from node" - likely a CTWA (Click-to-WhatsApp) ads message
|
||||
|
||||
@@ -50,6 +50,10 @@ export const BufferJSON = {
|
||||
export const getKeyAuthor = (key: WAMessageKey | undefined | null, meId = 'me') =>
|
||||
(key?.fromMe ? meId : key?.participantAlt || key?.remoteJidAlt || key?.participant || key?.remoteJid) || ''
|
||||
|
||||
export const isStringNullOrEmpty = (value: string | null | undefined): value is null | undefined | '' =>
|
||||
// eslint-disable-next-line eqeqeq
|
||||
value == null || value === ''
|
||||
|
||||
export const writeRandomPadMax16 = (msg: Uint8Array) => {
|
||||
const pad = randomBytes(1)
|
||||
const padLength = (pad[0]! & 0x0f) + 1
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Identity Change Handler
|
||||
*
|
||||
* Handles WhatsApp identity change notifications which occur when a contact's
|
||||
* encryption keys change. This typically happens when:
|
||||
* - User reinstalls WhatsApp
|
||||
* - User changes devices
|
||||
* - User's phone number verification expires and is re-verified
|
||||
*
|
||||
* This module centralizes the identity change logic to:
|
||||
* - Prevent duplicate session refreshes via debouncing
|
||||
* - Skip unnecessary refreshes for companion devices
|
||||
* - Handle offline notification scenarios correctly
|
||||
* - Provide clear result types for monitoring and debugging
|
||||
*
|
||||
* @module identity-change-handler
|
||||
* @see https://github.com/WhiskeySockets/Baileys/issues/2132
|
||||
*/
|
||||
|
||||
import NodeCache from '@cacheable/node-cache'
|
||||
import { areJidsSameUser, type BinaryNode, getBinaryNodeChild, jidDecode } from '../WABinary'
|
||||
import { isStringNullOrEmpty } from './generics'
|
||||
import type { ILogger } from './logger'
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Result type for identity change handling operations.
|
||||
* Each action variant indicates a specific outcome with relevant context.
|
||||
*/
|
||||
export type IdentityChangeResult =
|
||||
| { action: 'no_identity_node' }
|
||||
| { action: 'invalid_notification' }
|
||||
| { action: 'skipped_companion_device'; device: number }
|
||||
| { action: 'skipped_self_primary' }
|
||||
| { action: 'debounced' }
|
||||
| { action: 'skipped_offline' }
|
||||
| { action: 'session_refreshed'; hadExistingSession: boolean }
|
||||
| { action: 'session_refresh_failed'; error: unknown }
|
||||
|
||||
/**
|
||||
* Context required for identity change handling.
|
||||
* Provides access to session management and logging capabilities.
|
||||
*/
|
||||
export type IdentityChangeContext = {
|
||||
/** Current user's phone number JID (e.g., "5511999999999@s.whatsapp.net") */
|
||||
meId: string | undefined
|
||||
/** Current user's LID (Logical ID) if available */
|
||||
meLid: string | undefined
|
||||
/** Function to validate if a session exists for a JID */
|
||||
validateSession: (jid: string) => Promise<{ exists: boolean; reason?: string }>
|
||||
/** Function to assert/refresh sessions for given JIDs */
|
||||
assertSessions: (jids: string[], force?: boolean) => Promise<boolean>
|
||||
/** Cache for debouncing identity change processing */
|
||||
debounceCache: NodeCache<boolean>
|
||||
/** Logger instance for debugging and monitoring */
|
||||
logger: ILogger
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN HANDLER
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handles an identity change notification from WhatsApp.
|
||||
*
|
||||
* This function processes identity change events with the following logic:
|
||||
*
|
||||
* 1. **Validation**: Ensures the notification has required fields (from, identity node)
|
||||
* 2. **Companion Device Filter**: Skips notifications from companion devices (device > 0)
|
||||
* as they don't require session refresh
|
||||
* 3. **Self-Primary Skip**: Skips notifications for the current user's own identity
|
||||
* 4. **Debouncing**: Prevents duplicate processing for the same JID within TTL window
|
||||
* 5. **Offline Handling**: Skips refresh during offline notification processing
|
||||
* 6. **Session Refresh**: Attempts to refresh/create the session with force flag
|
||||
*
|
||||
* **Important**: Identity change notifications signal that we need to rebuild the session,
|
||||
* regardless of whether an existing session exists. This is critical for cases where
|
||||
* the local session was cleared (e.g., after key reset or device restore).
|
||||
*
|
||||
* @param node - The binary node containing the identity change notification
|
||||
* @param ctx - Context object with required dependencies
|
||||
* @returns Promise resolving to the result of the identity change handling
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await handleIdentityChange(notificationNode, {
|
||||
* meId: '5511999999999@s.whatsapp.net',
|
||||
* meLid: undefined,
|
||||
* validateSession: async (jid) => authState.keys.get('session', [jid]),
|
||||
* assertSessions: async (jids, force) => assertSession(jids, force),
|
||||
* debounceCache: new NodeCache({ stdTTL: 5 }),
|
||||
* logger: pino()
|
||||
* })
|
||||
*
|
||||
* switch (result.action) {
|
||||
* case 'session_refreshed':
|
||||
* console.log('Session refreshed, had existing:', result.hadExistingSession)
|
||||
* break
|
||||
* case 'session_refresh_failed':
|
||||
* console.error('Failed to refresh session:', result.error)
|
||||
* break
|
||||
* // ... handle other cases
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function handleIdentityChange(
|
||||
node: BinaryNode,
|
||||
ctx: IdentityChangeContext
|
||||
): Promise<IdentityChangeResult> {
|
||||
const from = node.attrs.from
|
||||
if (!from) {
|
||||
return { action: 'invalid_notification' }
|
||||
}
|
||||
|
||||
// Check for identity node presence
|
||||
const identityNode = getBinaryNodeChild(node, 'identity')
|
||||
if (!identityNode) {
|
||||
return { action: 'no_identity_node' }
|
||||
}
|
||||
|
||||
ctx.logger.info({ jid: from }, 'identity changed')
|
||||
|
||||
// Skip companion devices - they don't need session refresh
|
||||
const decoded = jidDecode(from)
|
||||
if (decoded?.device && decoded.device !== 0) {
|
||||
ctx.logger.debug(
|
||||
{ jid: from, device: decoded.device },
|
||||
'ignoring identity change from companion device'
|
||||
)
|
||||
return { action: 'skipped_companion_device', device: decoded.device }
|
||||
}
|
||||
|
||||
// Skip self-primary identity changes
|
||||
// FIX: Check meId and meLid independently to handle cases where only meLid exists
|
||||
const matchesMeId = ctx.meId && areJidsSameUser(from, ctx.meId)
|
||||
const matchesMeLid = ctx.meLid && areJidsSameUser(from, ctx.meLid)
|
||||
if (matchesMeId || matchesMeLid) {
|
||||
ctx.logger.info({ jid: from }, 'self primary identity changed')
|
||||
return { action: 'skipped_self_primary' }
|
||||
}
|
||||
|
||||
// Debounce to prevent duplicate processing
|
||||
// Check early but don't set yet - we only want to debounce actual refresh attempts
|
||||
if (ctx.debounceCache.get(from)) {
|
||||
ctx.logger.debug({ jid: from }, 'skipping identity assert (debounced)')
|
||||
return { action: 'debounced' }
|
||||
}
|
||||
|
||||
// Skip refresh during offline notification processing
|
||||
// Offline notifications are processed in batch and shouldn't trigger immediate refreshes
|
||||
// FIX: Check this BEFORE setting debounce cache to avoid incorrect debouncing
|
||||
const isOfflineNotification = !isStringNullOrEmpty(node.attrs.offline)
|
||||
if (isOfflineNotification) {
|
||||
ctx.logger.debug({ jid: from }, 'skipping session refresh during offline processing')
|
||||
return { action: 'skipped_offline' }
|
||||
}
|
||||
|
||||
// Check if we have an existing session (for logging purposes only)
|
||||
// FIX: We no longer skip refresh when no session exists - identity change is the
|
||||
// signal to rebuild the session, which is critical after key reset or device restore
|
||||
const hasExistingSession = await ctx.validateSession(from)
|
||||
|
||||
if (hasExistingSession.exists) {
|
||||
ctx.logger.debug({ jid: from }, 'existing session found, will refresh')
|
||||
} else {
|
||||
ctx.logger.debug({ jid: from }, 'no existing session, will create new session')
|
||||
}
|
||||
|
||||
// FIX: Set debounce cache only immediately before the actual refresh attempt
|
||||
// This ensures we don't incorrectly debounce when we exit early (offline, etc.)
|
||||
ctx.debounceCache.set(from, true)
|
||||
|
||||
// Attempt session refresh/creation
|
||||
try {
|
||||
await ctx.assertSessions([from], true)
|
||||
return { action: 'session_refreshed', hadExistingSession: hasExistingSession.exists }
|
||||
} catch (error) {
|
||||
ctx.logger.warn(
|
||||
{ error, jid: from },
|
||||
'failed to assert sessions after identity change'
|
||||
)
|
||||
return { action: 'session_refresh_failed', error }
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ export * from './process-message'
|
||||
export * from './message-retry-manager'
|
||||
export * from './browser-utils'
|
||||
|
||||
// === Identity and Session Management ===
|
||||
export * from './identity-change-handler'
|
||||
|
||||
// === Observability and Resilience Utilities ===
|
||||
|
||||
// Structured logging
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
import NodeCache from '@cacheable/node-cache'
|
||||
import { jest } from '@jest/globals'
|
||||
import P from 'pino'
|
||||
import { handleIdentityChange, type IdentityChangeContext, type IdentityChangeResult } from '../../Utils/identity-change-handler'
|
||||
import { type BinaryNode } from '../../WABinary'
|
||||
|
||||
const logger = P({ level: 'silent' })
|
||||
|
||||
type ValidateSessionFn = (jid: string) => Promise<{ exists: boolean; reason?: string }>
|
||||
type AssertSessionsFn = (jids: string[], force?: boolean) => Promise<boolean>
|
||||
|
||||
describe('Identity Change Handling', () => {
|
||||
let mockValidateSession: jest.Mock<ValidateSessionFn>
|
||||
let mockAssertSessions: jest.Mock<AssertSessionsFn>
|
||||
let identityAssertDebounce: NodeCache<boolean>
|
||||
let mockMeId: string | undefined
|
||||
let mockMeLid: string | undefined
|
||||
|
||||
function createIdentityChangeNode(from: string, offline?: string): BinaryNode {
|
||||
return {
|
||||
tag: 'notification',
|
||||
attrs: {
|
||||
from,
|
||||
type: 'encrypt',
|
||||
...(offline !== undefined ? { offline } : {})
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'identity',
|
||||
attrs: {},
|
||||
content: Buffer.from('test-identity-key')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function createContext(): IdentityChangeContext {
|
||||
return {
|
||||
meId: mockMeId,
|
||||
meLid: mockMeLid,
|
||||
validateSession: mockValidateSession,
|
||||
assertSessions: mockAssertSessions,
|
||||
debounceCache: identityAssertDebounce,
|
||||
logger
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockValidateSession = jest.fn()
|
||||
mockAssertSessions = jest.fn()
|
||||
identityAssertDebounce = new NodeCache<boolean>({ stdTTL: 5, useClones: false })
|
||||
mockMeId = 'myuser@s.whatsapp.net'
|
||||
mockMeLid = 'mylid@lid'
|
||||
})
|
||||
|
||||
describe('Core Checks', () => {
|
||||
it('should skip companion devices (device > 0)', async () => {
|
||||
const node = createIdentityChangeNode('user:5@s.whatsapp.net')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
expect(mockValidateSession).not.toHaveBeenCalled()
|
||||
expect(result.action).toBe('skipped_companion_device')
|
||||
})
|
||||
|
||||
it('should process primary device (device 0 or undefined)', async () => {
|
||||
mockValidateSession.mockResolvedValue({ exists: true })
|
||||
mockAssertSessions.mockResolvedValue(true)
|
||||
|
||||
const node = createIdentityChangeNode('user@s.whatsapp.net')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
expect(result.action).toBe('session_refreshed')
|
||||
expect((result as { hadExistingSession: boolean }).hadExistingSession).toBe(true)
|
||||
})
|
||||
|
||||
it('should skip self-primary identity (PN match)', async () => {
|
||||
const node = createIdentityChangeNode('myuser@s.whatsapp.net')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
expect(mockValidateSession).not.toHaveBeenCalled()
|
||||
expect(result.action).toBe('skipped_self_primary')
|
||||
})
|
||||
|
||||
it('should skip self-primary identity (LID match)', async () => {
|
||||
const node = createIdentityChangeNode('mylid@lid')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
expect(mockValidateSession).not.toHaveBeenCalled()
|
||||
expect(result.action).toBe('skipped_self_primary')
|
||||
})
|
||||
|
||||
it('should skip self-primary identity when only meLid exists', async () => {
|
||||
// FIX: Test for the case where meId is undefined but meLid matches
|
||||
mockMeId = undefined
|
||||
mockMeLid = 'mylid@lid'
|
||||
|
||||
const node = createIdentityChangeNode('mylid@lid')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
expect(mockValidateSession).not.toHaveBeenCalled()
|
||||
expect(result.action).toBe('skipped_self_primary')
|
||||
})
|
||||
|
||||
it('should create session when no existing session exists', async () => {
|
||||
// FIX: Identity change is the signal to rebuild session, even if none exists
|
||||
// This is critical for key reset or device restore scenarios
|
||||
mockValidateSession.mockResolvedValue({ exists: false })
|
||||
mockAssertSessions.mockResolvedValue(true)
|
||||
|
||||
const node = createIdentityChangeNode('user@s.whatsapp.net')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
expect(mockAssertSessions).toHaveBeenCalledWith(['user@s.whatsapp.net'], true)
|
||||
expect(result.action).toBe('session_refreshed')
|
||||
expect((result as { hadExistingSession: boolean }).hadExistingSession).toBe(false)
|
||||
})
|
||||
|
||||
it('should skip session refresh during offline processing', async () => {
|
||||
const node = createIdentityChangeNode('user@s.whatsapp.net', '0')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
// FIX: validateSession should not be called for offline notifications
|
||||
// because we skip before reaching that point
|
||||
expect(mockAssertSessions).not.toHaveBeenCalled()
|
||||
expect(result.action).toBe('skipped_offline')
|
||||
})
|
||||
|
||||
it('should refresh session when online with existing session', async () => {
|
||||
mockValidateSession.mockResolvedValue({ exists: true })
|
||||
mockAssertSessions.mockResolvedValue(true)
|
||||
|
||||
const node = createIdentityChangeNode('user@s.whatsapp.net')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
expect(mockAssertSessions).toHaveBeenCalledWith(['user@s.whatsapp.net'], true)
|
||||
expect(result.action).toBe('session_refreshed')
|
||||
expect((result as { hadExistingSession: boolean }).hadExistingSession).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Debounce', () => {
|
||||
it('should debounce multiple identity changes for the same JID', async () => {
|
||||
mockValidateSession.mockResolvedValue({ exists: true })
|
||||
mockAssertSessions.mockResolvedValue(true)
|
||||
|
||||
const node = createIdentityChangeNode('user@s.whatsapp.net')
|
||||
|
||||
const result1 = await handleIdentityChange(node, createContext())
|
||||
expect(result1.action).toBe('session_refreshed')
|
||||
|
||||
const result2 = await handleIdentityChange(node, createContext())
|
||||
expect(result2.action).toBe('debounced')
|
||||
expect(mockAssertSessions).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should allow different JIDs to process independently', async () => {
|
||||
mockValidateSession.mockResolvedValue({ exists: true })
|
||||
mockAssertSessions.mockResolvedValue(true)
|
||||
|
||||
const result1 = await handleIdentityChange(createIdentityChangeNode('user1@s.whatsapp.net'), createContext())
|
||||
const result2 = await handleIdentityChange(createIdentityChangeNode('user2@s.whatsapp.net'), createContext())
|
||||
|
||||
expect(result1.action).toBe('session_refreshed')
|
||||
expect(result2.action).toBe('session_refreshed')
|
||||
expect(mockAssertSessions).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should NOT set debounce cache when skipping due to offline', async () => {
|
||||
// FIX: Debounce should only be set when we actually attempt refresh
|
||||
const node = createIdentityChangeNode('user@s.whatsapp.net', '0')
|
||||
|
||||
const result1 = await handleIdentityChange(node, createContext())
|
||||
expect(result1.action).toBe('skipped_offline')
|
||||
|
||||
// Now process the same JID online - it should NOT be debounced
|
||||
mockValidateSession.mockResolvedValue({ exists: true })
|
||||
mockAssertSessions.mockResolvedValue(true)
|
||||
|
||||
const onlineNode = createIdentityChangeNode('user@s.whatsapp.net')
|
||||
const result2 = await handleIdentityChange(onlineNode, createContext())
|
||||
|
||||
expect(result2.action).toBe('session_refreshed')
|
||||
expect(mockAssertSessions).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle assertSessions failure gracefully', async () => {
|
||||
mockValidateSession.mockResolvedValue({ exists: true })
|
||||
const testError = new Error('Session assertion failed')
|
||||
mockAssertSessions.mockRejectedValue(testError)
|
||||
|
||||
const node = createIdentityChangeNode('user@s.whatsapp.net')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
expect(result.action).toBe('session_refresh_failed')
|
||||
expect((result as { error: unknown }).error).toBe(testError)
|
||||
})
|
||||
|
||||
it('should propagate validateSession errors', async () => {
|
||||
mockValidateSession.mockRejectedValue(new Error('Database error'))
|
||||
|
||||
const node = createIdentityChangeNode('user@s.whatsapp.net')
|
||||
await expect(handleIdentityChange(node, createContext())).rejects.toThrow('Database error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should return invalid_notification when from is missing', async () => {
|
||||
const node: BinaryNode = {
|
||||
tag: 'notification',
|
||||
attrs: { type: 'encrypt' },
|
||||
content: [{ tag: 'identity', attrs: {}, content: Buffer.from('key') }]
|
||||
}
|
||||
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
expect(result.action).toBe('invalid_notification')
|
||||
})
|
||||
|
||||
it('should return no_identity_node when identity child is missing', async () => {
|
||||
const node: BinaryNode = {
|
||||
tag: 'notification',
|
||||
attrs: { from: 'user@s.whatsapp.net', type: 'encrypt' },
|
||||
content: []
|
||||
}
|
||||
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
expect(result.action).toBe('no_identity_node')
|
||||
})
|
||||
|
||||
it('should handle LID JIDs correctly', async () => {
|
||||
mockValidateSession.mockResolvedValue({ exists: true })
|
||||
mockAssertSessions.mockResolvedValue(true)
|
||||
|
||||
const node = createIdentityChangeNode('12345@lid')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
expect(mockValidateSession).toHaveBeenCalledWith('12345@lid')
|
||||
expect(result.action).toBe('session_refreshed')
|
||||
})
|
||||
|
||||
it('should process when both meId and meLid are undefined', async () => {
|
||||
mockMeId = undefined
|
||||
mockMeLid = undefined
|
||||
mockValidateSession.mockResolvedValue({ exists: true })
|
||||
mockAssertSessions.mockResolvedValue(true)
|
||||
|
||||
const node = createIdentityChangeNode('anyuser@s.whatsapp.net')
|
||||
const result = await handleIdentityChange(node, createContext())
|
||||
|
||||
expect(result.action).toBe('session_refreshed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Result Types', () => {
|
||||
it('should include hadExistingSession in session_refreshed result', async () => {
|
||||
mockValidateSession.mockResolvedValue({ exists: true })
|
||||
mockAssertSessions.mockResolvedValue(true)
|
||||
|
||||
const node = createIdentityChangeNode('user@s.whatsapp.net')
|
||||
const result = await handleIdentityChange(node, createContext()) as Extract<IdentityChangeResult, { action: 'session_refreshed' }>
|
||||
|
||||
expect(result.action).toBe('session_refreshed')
|
||||
expect(result.hadExistingSession).toBe(true)
|
||||
})
|
||||
|
||||
it('should return hadExistingSession=false when creating new session', async () => {
|
||||
mockValidateSession.mockResolvedValue({ exists: false })
|
||||
mockAssertSessions.mockResolvedValue(true)
|
||||
|
||||
const node = createIdentityChangeNode('user@s.whatsapp.net')
|
||||
const result = await handleIdentityChange(node, createContext()) as Extract<IdentityChangeResult, { action: 'session_refreshed' }>
|
||||
|
||||
expect(result.action).toBe('session_refreshed')
|
||||
expect(result.hadExistingSession).toBe(false)
|
||||
})
|
||||
|
||||
it('should include device number in skipped_companion_device result', async () => {
|
||||
const node = createIdentityChangeNode('user:5@s.whatsapp.net')
|
||||
const result = await handleIdentityChange(node, createContext()) as Extract<IdentityChangeResult, { action: 'skipped_companion_device' }>
|
||||
|
||||
expect(result.action).toBe('skipped_companion_device')
|
||||
expect(result.device).toBe(5)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user