From 73edcd29cce2c7f11ad5eed94a741fa99d997683 Mon Sep 17 00:00:00 2001 From: Bharath Balan <62698609+bhabalan@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:59:45 +0530 Subject: [PATCH 1/3] feat(call-control): enhance agent name display logic in multi-agent conferences --- .../src/components/task/task.types.ts | 4 +- packages/contact-center/task/src/helper.ts | 50 ++- packages/contact-center/task/tests/helper.ts | 364 ++++++++++++++++++ .../task/tests/utils/task-util.ts | 205 ++++++++++ 4 files changed, 617 insertions(+), 6 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index cc69874c8..d8a365173 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -414,12 +414,12 @@ export interface ControlProps { /** * Function to set the last target type */ - lastTargetType: 'queue' | 'agent'; + lastTargetType: 'queue' | 'agent' | 'entryPoint' | 'dialNumber'; /** * Function to set the last target type */ - setLastTargetType: (targetType: 'queue' | 'agent') => void; + setLastTargetType: (targetType: 'queue' | 'agent' | 'entryPoint' | 'dialNumber') => void; controlVisibility: ControlVisibility; diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index c09e50502..8b6a1ff31 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -302,7 +302,7 @@ export const useCallControl = (props: useCallControlProps) => { // Consult timer labels and timestamps const [consultTimerLabel, setConsultTimerLabel] = useState(TIMER_LABEL_CONSULTING); const [consultTimerTimestamp, setConsultTimerTimestamp] = useState(0); - const [lastTargetType, setLastTargetType] = useState<'agent' | 'queue'>('agent'); + const [lastTargetType, setLastTargetType] = useState<'agent' | 'queue' | 'entryPoint' | 'dialNumber'>('agent'); const [conferenceParticipants, setConferenceParticipants] = useState([]); // Use custom hook for hold timer management @@ -319,6 +319,19 @@ export const useCallControl = (props: useCallControlProps) => { try { if (!currentTask?.data?.interaction?.participants) return; + // Skip extraction if consulting to Entry Point or Dial Number + // The name is already set correctly via handleTargetSelect + if (lastTargetType === 'entryPoint' || lastTargetType === 'dialNumber') { + logger.info( + `Skipping agent extraction for ${lastTargetType} consult - using pre-set name: ${consultAgentName}`, + { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + } + ); + return; + } + const {interaction} = currentTask.data; const myAgentId = store.cc.agentConfig?.agentId; @@ -328,8 +341,37 @@ export const useCallControl = (props: useCallControlProps) => { (participant as Participant).pType === 'Agent' && (participant as Participant).id !== myAgentId ); - // Pick the first other agent (should only be one in a consult) - const foundAgent = otherAgents.length > 0 ? {id: otherAgents[0].id, name: otherAgents[0].name} : null; + // In a conference with multiple agents, find the agent currently being consulted + // Priority: 1) consultState="consulting" 2) most recent consultTimestamp 3) isConsulted=true + let foundAgent: {id: string; name: string} | null = null; + + if (otherAgents.length > 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const consultingAgent = otherAgents.find((agent: any) => agent.consultState === 'consulting'); + + if (consultingAgent) { + // Found an agent currently in "consulting" state - this is the active consult + foundAgent = { + id: consultingAgent.id, + name: consultingAgent.name, + }; + } else { + // Fallback: Find agent with most recent consultTimestamp + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const agentWithMostRecentTimestamp = otherAgents.reduce((latest: any, current: any) => { + const currentTimestamp = current.consultTimestamp || current.joinTimestamp || 0; + const latestTimestamp = latest ? latest.consultTimestamp || latest.joinTimestamp || 0 : 0; + return currentTimestamp > latestTimestamp ? current : latest; + }, null); + + if (agentWithMostRecentTimestamp) { + foundAgent = { + id: agentWithMostRecentTimestamp.id, + name: agentWithMostRecentTimestamp.name, + }; + } + } + } if (foundAgent) { setConsultAgentName(foundAgent.name); @@ -345,7 +387,7 @@ export const useCallControl = (props: useCallControlProps) => { method: 'extractConsultingAgent', }); } - }, [currentTask, logger]); + }, [currentTask, logger, lastTargetType, consultAgentName]); // Extract main call timestamp whenever currentTask changes useEffect(() => { diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index 725959c80..67bf6cf5d 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -3694,6 +3694,370 @@ describe('useCallControl', () => { expect(result.current.consultTimerTimestamp).toBe(2000); }); + it('should select agent with consultState="consulting" in multi-agent conference', async () => { + const mockTaskWithMultiAgentConference = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interaction: { + ...mockCurrentTask.data.interaction, + media: { + main: { + mType: 'telephony', + isHold: false, + mediaResourceId: 'main', + participants: ['agent2', 'agent3', 'agent4', 'customer1'], + }, + }, + participants: { + agent2: { + id: 'agent2', + name: 'Agent 2', + pType: 'Agent', + joinTimestamp: 1000, + consultTimestamp: 2000, + consultState: 'conferencing', + }, + agent3: { + id: 'agent3', + name: 'Agent 3', + pType: 'Agent', + joinTimestamp: 3000, + consultTimestamp: 4000, + consultState: 'conferencing', + }, + agent4: { + id: 'agent4', + name: 'Agent 4', + pType: 'Agent', + joinTimestamp: 5000, + consultTimestamp: 6000, + consultState: 'consulting', + isConsulted: true, + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + joinTimestamp: 500, + }, + }, + }, + }, + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockTaskWithMultiAgentConference, + logger, + deviceType: 'BROWSER', + featureFlags: {}, + isMuted: false, + conferenceEnabled: true, + agentId: 'agent2', + }) + ); + + await waitFor(() => { + // Should select Agent 4 as they have consultState="consulting" + expect(result.current.consultAgentName).toBe('Agent 4'); + }); + }); + + it('should fallback to most recent timestamp when no agent has consultState="consulting"', async () => { + const mockTaskWithMultiAgentConference = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interaction: { + ...mockCurrentTask.data.interaction, + media: { + main: { + mType: 'telephony', + isHold: false, + mediaResourceId: 'main', + participants: ['agent2', 'agent3', 'agent4', 'customer1'], + }, + }, + participants: { + agent2: { + id: 'agent2', + name: 'Agent 2', + pType: 'Agent', + joinTimestamp: 1000, + consultTimestamp: 2000, + }, + agent3: { + id: 'agent3', + name: 'Agent 3', + pType: 'Agent', + joinTimestamp: 3000, + consultTimestamp: 4000, + }, + agent4: { + id: 'agent4', + name: 'Agent 4', + pType: 'Agent', + joinTimestamp: 5000, + consultTimestamp: 6000, + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + joinTimestamp: 500, + }, + }, + }, + }, + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockTaskWithMultiAgentConference, + logger, + deviceType: 'BROWSER', + featureFlags: {}, + isMuted: false, + conferenceEnabled: true, + agentId: 'agent2', + }) + ); + + await waitFor(() => { + // Should select Agent 4 as they have the most recent consultTimestamp (6000) + expect(result.current.consultAgentName).toBe('Agent 4'); + }); + }); + + it('should correctly identify single agent in simple consult scenario', async () => { + const mockTaskWithSingleConsult = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interaction: { + ...mockCurrentTask.data.interaction, + media: { + main: { + mType: 'telephony', + isHold: false, + mediaResourceId: 'main', + participants: ['agent2', 'agent3', 'customer1'], + }, + }, + participants: { + agent2: { + id: 'agent2', + name: 'Agent 2', + pType: 'Agent', + joinTimestamp: 1000, + }, + agent3: { + id: 'agent3', + name: 'Agent 3', + pType: 'Agent', + joinTimestamp: 3000, + consultTimestamp: 4000, + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + joinTimestamp: 500, + }, + }, + }, + }, + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockTaskWithSingleConsult, + logger, + deviceType: 'BROWSER', + featureFlags: {}, + isMuted: false, + conferenceEnabled: false, + agentId: 'agent2', + }) + ); + + await waitFor(() => { + // Should select Agent 3 as they are the only other agent + expect(result.current.consultAgentName).toBe('Agent 3'); + }); + }); + + it('should handle agents without timestamps (backward compatibility)', async () => { + const mockTaskWithoutTimestamps = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interaction: { + ...mockCurrentTask.data.interaction, + participants: { + agent2: { + id: 'agent2', + name: 'Agent 2', + pType: 'Agent', + }, + agent3: { + id: 'agent3', + name: 'Agent 3', + pType: 'Agent', + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + }, + }, + }, + }, + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockTaskWithoutTimestamps, + logger, + deviceType: 'BROWSER', + featureFlags: {}, + isMuted: false, + conferenceEnabled: false, + agentId: 'agent2', + }) + ); + + await waitFor(() => { + // Should select Agent 3 (first other agent found) when no timestamps are available + expect(result.current.consultAgentName).toBe('Agent 3'); + }); + }); + + it('should preserve entry point name when consulting to entry point', async () => { + const mockTaskWithAgents = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interaction: { + ...mockCurrentTask.data.interaction, + participants: { + agent2: { + id: 'agent2', + name: 'Agent 2', + pType: 'Agent', + consultState: 'conferencing', + consultTimestamp: 1000, + }, + agent3: { + id: 'agent3', + name: 'Agent 3', + pType: 'Agent', + consultState: 'conferencing', + consultTimestamp: 2000, + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + }, + }, + }, + }, + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockTaskWithAgents, + logger, + deviceType: 'BROWSER', + featureFlags: {}, + isMuted: false, + conferenceEnabled: true, + agentId: 'agent2', + }) + ); + + // Simulate setting the entry point name (as done by handleTargetSelect) + act(() => { + result.current.setConsultAgentName('Support Entry Point'); + result.current.setLastTargetType('entryPoint'); + }); + + // Wait to ensure extractConsultingAgent doesn't override the name + await waitFor( + () => { + expect(result.current.consultAgentName).toBe('Support Entry Point'); + }, + {timeout: 1000} + ); + + // Verify the name is still the entry point name and wasn't overridden by agent extraction + expect(result.current.consultAgentName).toBe('Support Entry Point'); + }); + + it('should preserve dial number when consulting to dial number', async () => { + const mockTaskWithAgents = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interaction: { + ...mockCurrentTask.data.interaction, + participants: { + agent2: { + id: 'agent2', + name: 'Agent 2', + pType: 'Agent', + consultState: 'conferencing', + consultTimestamp: 1000, + }, + agent3: { + id: 'agent3', + name: 'Agent 3', + pType: 'Agent', + consultState: 'conferencing', + consultTimestamp: 2000, + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + }, + }, + }, + }, + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockTaskWithAgents, + logger, + deviceType: 'BROWSER', + featureFlags: {}, + isMuted: false, + conferenceEnabled: true, + agentId: 'agent2', + }) + ); + + // Simulate setting the dial number (as done by handleTargetSelect) + act(() => { + result.current.setConsultAgentName('+1234567890'); + result.current.setLastTargetType('dialNumber'); + }); + + // Wait to ensure extractConsultingAgent doesn't override the name + await waitFor( + () => { + expect(result.current.consultAgentName).toBe('+1234567890'); + }, + {timeout: 1000} + ); + + // Verify the name is still the dial number and wasn't overridden by agent extraction + expect(result.current.consultAgentName).toBe('+1234567890'); + }); + it('should preserve consult timer when resuming from hold', async () => { const mockTaskWithConsultHeld = { ...mockCurrentTask, diff --git a/packages/contact-center/task/tests/utils/task-util.ts b/packages/contact-center/task/tests/utils/task-util.ts index 1083db07d..67a8f01b3 100644 --- a/packages/contact-center/task/tests/utils/task-util.ts +++ b/packages/contact-center/task/tests/utils/task-util.ts @@ -421,6 +421,211 @@ describe('getControlsVisibility', () => { consultCallHeld: false, }); }); + + it('should enable end button when in conference and switched back from consult (consultCallHeld = true)', () => { + const deviceType = 'BROWSER'; + const featureFlags = { + isEndCallEnabled: true, + isEndConsultEnabled: true, + webRtcEnabled: true, + }; + + // Mock a task with conference in progress and consult call held + const task = { + ...mockTask, + data: { + ...mockTask.data, + isConferenceInProgress: true, + consultMediaResourceId: 'consult', + interaction: { + ...mockTask.data.interaction, + mediaType: 'telephony', + state: 'conferencing', // Conference state + media: { + main: { + mediaResourceId: 'main', + mType: 'mainCall', + isHold: false, + participants: ['agent1', 'agent2', 'customer1'], + }, + consult: { + mediaResourceId: 'consult', + mType: 'consult', + isHold: true, // Consult is on hold - we've switched back to main + participants: ['agent1', 'agent3'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + name: 'Agent One', + consultState: 'Conferencing', + isConsulted: false, + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + hasLeft: false, + }, + agent3: { + id: 'agent3', + pType: 'Agent', + name: 'Agent Three', + hasLeft: false, + }, + customer1: { + id: 'customer1', + pType: 'Customer', + name: 'Customer', + hasLeft: false, + }, + }, + }, + }, + } as ITask; + + const result = getControlsVisibility(deviceType, featureFlags, task, 'agent1', true); + + // End button should be enabled when switched back to main call from consult + expect(result.end.isEnabled).toBe(true); + expect(result.end.isVisible).toBe(true); + }); + + it('should enable end button when in regular consult and switched back to main call (consultCallHeld = true)', () => { + const deviceType = 'BROWSER'; + const featureFlags = { + isEndCallEnabled: true, + isEndConsultEnabled: true, + webRtcEnabled: true, + }; + + // Mock a task with consult (not conference) and consult call held + const task = { + ...mockTask, + data: { + ...mockTask.data, + isConferenceInProgress: false, + consultMediaResourceId: 'consult', + interaction: { + ...mockTask.data.interaction, + mediaType: 'telephony', + state: 'consulting', // Consult state + media: { + main: { + mediaResourceId: 'main', + mType: 'mainCall', + isHold: false, + participants: ['agent1', 'customer1'], + }, + consult: { + mediaResourceId: 'consult', + mType: 'consult', + isHold: true, // Consult is on hold - we've switched back to main + participants: ['agent1', 'agent2'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + name: 'Agent One', + consultState: 'Initiated', + isConsulted: false, + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + hasLeft: false, + }, + customer1: { + id: 'customer1', + pType: 'Customer', + name: 'Customer', + hasLeft: false, + }, + }, + }, + }, + } as ITask; + + const result = getControlsVisibility(deviceType, featureFlags, task, 'agent1', false); + + // End button should be enabled when switched back to main call from consult + expect(result.end.isEnabled).toBe(true); + expect(result.end.isVisible).toBe(true); + }); + + it('should disable end button when consult is active (not on hold)', () => { + const deviceType = 'BROWSER'; + const featureFlags = { + isEndCallEnabled: true, + isEndConsultEnabled: true, + webRtcEnabled: true, + }; + + // Mock a task with active consult (not on hold) + const task = { + ...mockTask, + data: { + ...mockTask.data, + isConferenceInProgress: false, + consultMediaResourceId: 'consult', + interaction: { + ...mockTask.data.interaction, + mediaType: 'telephony', + state: 'consulting', // Active consult state + media: { + main: { + mediaResourceId: 'main', + mType: 'mainCall', + isHold: true, // Main is on hold - we're on consult call + participants: ['agent1', 'customer1'], + }, + consult: { + mediaResourceId: 'consult', + mType: 'consult', + isHold: false, // Consult is active + participants: ['agent1', 'agent2'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + name: 'Agent One', + consultState: 'Initiated', // Indicate consult was initiated + isConsulted: false, + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + isConsulted: false, + hasLeft: false, + }, + customer1: { + id: 'customer1', + pType: 'Customer', + name: 'Customer', + hasLeft: false, + }, + }, + }, + }, + } as ITask; + + const result = getControlsVisibility(deviceType, featureFlags, task, 'agent1', false); + + // End button should be disabled when on active consult call + expect(result.end.isEnabled).toBe(false); + expect(result.end.isVisible).toBe(true); + }); }); describe('findHoldTimestamp', () => { From 4b8473702bffc1a5d062ac5fbe5ff41cdb195779 Mon Sep 17 00:00:00 2001 From: Bharath Balan <62698609+bhabalan@users.noreply.github.com> Date: Mon, 22 Dec 2025 09:32:23 +0530 Subject: [PATCH 2/3] fix(call-control): improve agent name extraction logic during consults --- packages/contact-center/task/src/helper.ts | 136 ++++++++++++++------- 1 file changed, 92 insertions(+), 44 deletions(-) diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index 8b6a1ff31..d7b19fcbd 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -319,66 +319,114 @@ export const useCallControl = (props: useCallControlProps) => { try { if (!currentTask?.data?.interaction?.participants) return; - // Skip extraction if consulting to Entry Point or Dial Number - // The name is already set correctly via handleTargetSelect + const {interaction} = currentTask.data; + const myAgentId = store.cc.agentConfig?.agentId; + + // For Entry Point or Dial Number consults, check if destination agent has joined if (lastTargetType === 'entryPoint' || lastTargetType === 'dialNumber') { - logger.info( - `Skipping agent extraction for ${lastTargetType} consult - using pre-set name: ${consultAgentName}`, - { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const consultDestinationAgentName = (interaction as any).callProcessingDetails?.consultDestinationAgentName; + + if (consultDestinationAgentName) { + // Destination agent has joined, show their name + setConsultAgentName(consultDestinationAgentName); + logger.info(`${lastTargetType} consult answered - showing agent name: ${consultDestinationAgentName}`, { module: 'widget-cc-task#helper.ts', method: 'useCallControl#extractConsultingAgent', + }); + } else { + // Still ringing - find the EP/DN participant in the consult media + const consultMediaResourceId = findMediaResourceId(currentTask, 'consult'); + + if (consultMediaResourceId && interaction.media?.[consultMediaResourceId]) { + const consultMedia = interaction.media[consultMediaResourceId]; + // Find the participant in consult media who is not the current agent + const consultParticipantId = consultMedia.participants?.find( + (participantId: string) => participantId !== myAgentId + ); + + if (consultParticipantId && interaction.participants[consultParticipantId]) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const participant = interaction.participants[consultParticipantId] as any; + const phoneNumber = participant.dn || participant.id; + + if (phoneNumber && phoneNumber !== consultAgentName) { + setConsultAgentName(phoneNumber); + logger.info(`${lastTargetType} consult ringing - showing phone number: ${phoneNumber}`, { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + }); + } + } } - ); + } return; } - const {interaction} = currentTask.data; - const myAgentId = store.cc.agentConfig?.agentId; + // For regular agent consults, find the agent in the consult media + const consultMediaResourceId = findMediaResourceId(currentTask, 'consult'); - // Find all agent participants except the current agent - const otherAgents = Object.values(interaction.participants || {}).filter( - (participant): participant is Participant => - (participant as Participant).pType === 'Agent' && (participant as Participant).id !== myAgentId - ); + if (consultMediaResourceId && interaction.media?.[consultMediaResourceId]) { + const consultMedia = interaction.media[consultMediaResourceId]; + // Find the agent participant in consult media who is not the current agent + const consultParticipantId = consultMedia.participants?.find((participantId: string) => { + const participant = interaction.participants[participantId]; + return participant && participant.id !== myAgentId && participant.pType === 'Agent'; + }); - // In a conference with multiple agents, find the agent currently being consulted - // Priority: 1) consultState="consulting" 2) most recent consultTimestamp 3) isConsulted=true - let foundAgent: {id: string; name: string} | null = null; + if (consultParticipantId && interaction.participants[consultParticipantId]) { + const consultAgent = interaction.participants[consultParticipantId]; + setConsultAgentName(consultAgent.name || consultAgent.id); + logger.info(`Consulting agent detected: ${consultAgent.name} ${consultAgent.id}`, { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + }); + } + } else { + // Fallback: Use old logic if consult media not found + const otherAgents = Object.values(interaction.participants || {}).filter( + (participant): participant is Participant => + (participant as Participant).pType === 'Agent' && (participant as Participant).id !== myAgentId + ); - if (otherAgents.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const consultingAgent = otherAgents.find((agent: any) => agent.consultState === 'consulting'); - - if (consultingAgent) { - // Found an agent currently in "consulting" state - this is the active consult - foundAgent = { - id: consultingAgent.id, - name: consultingAgent.name, - }; - } else { - // Fallback: Find agent with most recent consultTimestamp + // In a conference with multiple agents, find the agent currently being consulted + // Priority: 1) consultState="consulting" 2) most recent consultTimestamp + let foundAgent: {id: string; name: string} | null = null; + + if (otherAgents.length > 0) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const agentWithMostRecentTimestamp = otherAgents.reduce((latest: any, current: any) => { - const currentTimestamp = current.consultTimestamp || current.joinTimestamp || 0; - const latestTimestamp = latest ? latest.consultTimestamp || latest.joinTimestamp || 0 : 0; - return currentTimestamp > latestTimestamp ? current : latest; - }, null); + const consultingAgent = otherAgents.find((agent: any) => agent.consultState === 'consulting'); - if (agentWithMostRecentTimestamp) { + if (consultingAgent) { foundAgent = { - id: agentWithMostRecentTimestamp.id, - name: agentWithMostRecentTimestamp.name, + id: consultingAgent.id, + name: consultingAgent.name, }; + } else { + // Fallback: Find agent with most recent consultTimestamp + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const agentWithMostRecentTimestamp = otherAgents.reduce((latest: any, current: any) => { + const currentTimestamp = current.consultTimestamp || current.joinTimestamp || 0; + const latestTimestamp = latest ? latest.consultTimestamp || latest.joinTimestamp || 0 : 0; + return currentTimestamp > latestTimestamp ? current : latest; + }, null); + + if (agentWithMostRecentTimestamp) { + foundAgent = { + id: agentWithMostRecentTimestamp.id, + name: agentWithMostRecentTimestamp.name, + }; + } } } - } - if (foundAgent) { - setConsultAgentName(foundAgent.name); - logger.info(`Consulting agent detected: ${foundAgent.name} ${foundAgent.id}`, { - module: 'widget-cc-task#helper.ts', - method: 'useCallControl#extractConsultingAgent', - }); + if (foundAgent) { + setConsultAgentName(foundAgent.name); + logger.info(`Consulting agent detected (fallback): ${foundAgent.name} ${foundAgent.id}`, { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + }); + } } } catch (error) { console.log('error', error); @@ -387,7 +435,7 @@ export const useCallControl = (props: useCallControlProps) => { method: 'extractConsultingAgent', }); } - }, [currentTask, logger, lastTargetType, consultAgentName]); + }, [currentTask, logger, lastTargetType, consultAgentName, setConsultAgentName]); // Extract main call timestamp whenever currentTask changes useEffect(() => { From acd688ca0a96db52b0fdf76c742e950688b190c4 Mon Sep 17 00:00:00 2001 From: Bharath Balan <62698609+bhabalan@users.noreply.github.com> Date: Mon, 5 Jan 2026 07:16:29 +0530 Subject: [PATCH 3/3] feat(call-control): update tests for agent name display logic in MPC --- .../src/components/task/task.types.ts | 16 +- .../CallControl/call-control.snapshot.tsx | 4 +- .../task/CallControl/call-control.tsx | 4 +- .../call-control-cad.snapshot.tsx | 4 +- .../task/CallControlCAD/call-control-cad.tsx | 4 +- .../task/src/Utils/task-util.ts | 11 +- packages/contact-center/task/src/helper.ts | 15 +- .../contact-center/task/src/task.types.ts | 12 + .../task/tests/CallControl/index.tsx | 3 +- .../task/tests/CallControlCAD/index.tsx | 9 +- packages/contact-center/task/tests/helper.ts | 471 +++++++++++++++++- 11 files changed, 522 insertions(+), 31 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index 94609495d..7a0a2c65c 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -16,6 +16,18 @@ import { type Enum> = T[keyof T]; +/** + * Target types for consult/transfer operations + */ +export const TARGET_TYPE = { + AGENT: 'agent', + QUEUE: 'queue', + ENTRY_POINT: 'entryPoint', + DIAL_NUMBER: 'dialNumber', +} as const; + +export type TargetType = (typeof TARGET_TYPE)[keyof typeof TARGET_TYPE]; + /** * Interface representing the TaskProps of a user. */ @@ -414,12 +426,12 @@ export interface ControlProps { /** * Function to set the last target type */ - lastTargetType: 'queue' | 'agent' | 'entryPoint' | 'dialNumber'; + lastTargetType: TargetType; /** * Function to set the last target type */ - setLastTargetType: (targetType: 'queue' | 'agent' | 'entryPoint' | 'dialNumber') => void; + setLastTargetType: (targetType: TargetType) => void; controlVisibility: ControlVisibility; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx index 574c62eb3..949b6e22b 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx @@ -2,7 +2,7 @@ import React from 'react'; import '@testing-library/jest-dom'; import {render, fireEvent, act} from '@testing-library/react'; import CallControlComponent from '../../../../src/components/task/CallControl/call-control'; -import {CallControlComponentProps} from '../../../../src/components/task/task.types'; +import {CallControlComponentProps, TARGET_TYPE} from '../../../../src/components/task/task.types'; import {mockTask, mockAgents, mockProfile, mockCC} from '@webex/test-fixtures'; import {BuddyDetails, IWrapupCode} from '@webex/cc-store'; @@ -99,7 +99,7 @@ describe('CallControlComponent Snapshots', () => { consultTimerLabel: 'Consulting', consultTimerTimestamp: 0, allowConsultToQueue: mockProfile.allowConsultToQueue, - lastTargetType: 'agent', + lastTargetType: TARGET_TYPE.AGENT, setLastTargetType: jest.fn(), controlVisibility: { accept: {isVisible: true, isEnabled: true}, diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx index ce20e8776..7037ec59f 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {render, fireEvent} from '@testing-library/react'; import '@testing-library/jest-dom'; import CallControlComponent from '../../../../src/components/task/CallControl/call-control'; -import {CallControlComponentProps, CallControlMenuType} from '../../../../src/components/task/task.types'; +import {CallControlComponentProps, CallControlMenuType, TARGET_TYPE} from '../../../../src/components/task/task.types'; import * as callControlUtils from '../../../../src/components/task/CallControl/call-control.utils'; import {mockTask} from '@webex/test-fixtures'; @@ -121,7 +121,7 @@ describe('CallControlComponent', () => { consultTimerLabel: 'Consulting', consultTimerTimestamp: 0, allowConsultToQueue: true, - lastTargetType: 'agent', + lastTargetType: TARGET_TYPE.AGENT, setLastTargetType: jest.fn(), controlVisibility: mockControlVisibility, logger: mockLogger, diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx index aad10baf6..0b4277f88 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {render} from '@testing-library/react'; import CallControlCADComponent from '../../../../src/components/task/CallControlCAD/call-control-cad'; -import {CallControlComponentProps} from '../../../../src/components/task/task.types'; +import {CallControlComponentProps, TARGET_TYPE} from '../../../../src/components/task/task.types'; import {mockTask} from '@webex/test-fixtures'; import {BuddyDetails} from '@webex/cc-store'; import '@testing-library/jest-dom'; @@ -131,7 +131,7 @@ describe('CallControlCADComponent Snapshots', () => { consultTimerLabel: 'Consulting', consultTimerTimestamp: 0, allowConsultToQueue: true, - lastTargetType: 'agent', + lastTargetType: TARGET_TYPE.AGENT, setLastTargetType: jest.fn(), controlVisibility: { accept: {isVisible: true, isEnabled: true}, diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx index e6e048737..95af309f8 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {render} from '@testing-library/react'; import CallControlCADComponent from '../../../../src/components/task/CallControlCAD/call-control-cad'; -import {CallControlComponentProps} from '../../../../src/components/task/task.types'; +import {CallControlComponentProps, TARGET_TYPE} from '../../../../src/components/task/task.types'; import {mockTask} from '@webex/test-fixtures'; import {BuddyDetails} from '@webex/cc-store'; import '@testing-library/jest-dom'; @@ -141,7 +141,7 @@ describe('CallControlCADComponent', () => { consultTimerLabel: 'Consulting', consultTimerTimestamp: 0, allowConsultToQueue: true, - lastTargetType: 'agent', + lastTargetType: TARGET_TYPE.AGENT, setLastTargetType: jest.fn(), controlVisibility: mockControlVisibility, logger: mockLogger, diff --git a/packages/contact-center/task/src/Utils/task-util.ts b/packages/contact-center/task/src/Utils/task-util.ts index 41314c297..fe87c4eff 100644 --- a/packages/contact-center/task/src/Utils/task-util.ts +++ b/packages/contact-center/task/src/Utils/task-util.ts @@ -86,12 +86,14 @@ export function getEndButtonVisibility( isConsultInitiatedOrAcceptedOrBeingConsulted: boolean, isConferenceInProgress: boolean, isConsultCompleted: boolean, - isHeld: boolean + isHeld: boolean, + consultCallHeld: boolean ): Visibility { const isVisible = isBrowser || (isEndCallEnabled && isCall) || !isCall; - // Disable if: held (except when in conference and consult not completed) OR consult in progress + // Disable if: held (except when in conference and consult not completed) OR consult in progress (unless consult call is held - meaning we're back on main) const isEnabled = - (!isHeld || (isConferenceInProgress && !isConsultCompleted)) && !isConsultInitiatedOrAcceptedOrBeingConsulted; + (!isHeld || (isConferenceInProgress && !isConsultCompleted)) && + (!isConsultInitiatedOrAcceptedOrBeingConsulted || consultCallHeld); return {isVisible, isEnabled}; } @@ -441,7 +443,8 @@ export function getControlsVisibility( isConsultInitiatedOrAcceptedOrBeingConsulted, isConferenceInProgress, isConsultCompleted, - isHeld + isHeld, + consultCallHeld ), muteUnmute: getMuteUnmuteButtonVisibility(isBrowser, webRtcEnabled, isCall, isBeingConsulted), holdResume: getHoldResumeButtonVisibility( diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index d7b19fcbd..ac1144099 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -1,6 +1,13 @@ import {useEffect, useCallback, useState, useMemo} from 'react'; import {AddressBookEntriesResponse, AddressBookEntrySearchParams, ITask} from '@webex/contact-center'; -import {useCallControlProps, UseTaskListProps, UseTaskProps, useOutdialCallProps} from './task.types'; +import { + useCallControlProps, + UseTaskListProps, + UseTaskProps, + useOutdialCallProps, + TargetType, + TARGET_TYPE, +} from './task.types'; import store, { TASK_EVENTS, BuddyDetails, @@ -302,7 +309,7 @@ export const useCallControl = (props: useCallControlProps) => { // Consult timer labels and timestamps const [consultTimerLabel, setConsultTimerLabel] = useState(TIMER_LABEL_CONSULTING); const [consultTimerTimestamp, setConsultTimerTimestamp] = useState(0); - const [lastTargetType, setLastTargetType] = useState<'agent' | 'queue' | 'entryPoint' | 'dialNumber'>('agent'); + const [lastTargetType, setLastTargetType] = useState(TARGET_TYPE.AGENT); const [conferenceParticipants, setConferenceParticipants] = useState([]); // Use custom hook for hold timer management @@ -323,7 +330,7 @@ export const useCallControl = (props: useCallControlProps) => { const myAgentId = store.cc.agentConfig?.agentId; // For Entry Point or Dial Number consults, check if destination agent has joined - if (lastTargetType === 'entryPoint' || lastTargetType === 'dialNumber') { + if (lastTargetType === TARGET_TYPE.ENTRY_POINT || lastTargetType === TARGET_TYPE.DIAL_NUMBER) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const consultDestinationAgentName = (interaction as any).callProcessingDetails?.consultDestinationAgentName; @@ -408,7 +415,7 @@ export const useCallControl = (props: useCallControlProps) => { const agentWithMostRecentTimestamp = otherAgents.reduce((latest: any, current: any) => { const currentTimestamp = current.consultTimestamp || current.joinTimestamp || 0; const latestTimestamp = latest ? latest.consultTimestamp || latest.joinTimestamp || 0 : 0; - return currentTimestamp > latestTimestamp ? current : latest; + return currentTimestamp >= latestTimestamp ? current : latest; }, null); if (agentWithMostRecentTimestamp) { diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index 8af46061e..c0c759382 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -48,3 +48,15 @@ export interface DeviceTypeFlags { isAgentDN: boolean; isExtension: boolean; } + +/** + * Target types for consult/transfer operations + */ +export const TARGET_TYPE = { + AGENT: 'agent', + QUEUE: 'queue', + ENTRY_POINT: 'entryPoint', + DIAL_NUMBER: 'dialNumber', +} as const; + +export type TargetType = (typeof TARGET_TYPE)[keyof typeof TARGET_TYPE]; diff --git a/packages/contact-center/task/tests/CallControl/index.tsx b/packages/contact-center/task/tests/CallControl/index.tsx index 3f92280ce..aee5e3723 100644 --- a/packages/contact-center/task/tests/CallControl/index.tsx +++ b/packages/contact-center/task/tests/CallControl/index.tsx @@ -4,6 +4,7 @@ import * as helper from '../../src/helper'; import {CallControl} from '../../src'; import store from '@webex/cc-store'; import {mockTask} from '@webex/test-fixtures'; +import {TARGET_TYPE} from '../../src/task.types'; import '@testing-library/jest-dom'; const onHoldResumeCb = jest.fn(); @@ -46,7 +47,7 @@ describe('CallControl Component', () => { setConsultAgentName: jest.fn(), holdTime: 0, startTimestamp: 0, - lastTargetType: 'agent' as const, + lastTargetType: TARGET_TYPE.AGENT, setLastTargetType: jest.fn(), controlVisibility: { accept: defaultVisibility, diff --git a/packages/contact-center/task/tests/CallControlCAD/index.tsx b/packages/contact-center/task/tests/CallControlCAD/index.tsx index 2a0d2908a..60ae4f7df 100644 --- a/packages/contact-center/task/tests/CallControlCAD/index.tsx +++ b/packages/contact-center/task/tests/CallControlCAD/index.tsx @@ -4,6 +4,7 @@ import * as helper from '../../src/helper'; import {CallControlCAD} from '../../src'; import store from '@webex/cc-store'; import {mockTask} from '@webex/test-fixtures'; +import {TARGET_TYPE} from '../../src/task.types'; import '@testing-library/jest-dom'; const onHoldResumeCb = jest.fn(); @@ -42,7 +43,7 @@ describe('CallControlCAD Component', () => { setConsultAgentName: jest.fn(), holdTime: 0, startTimestamp: 0, - lastTargetType: 'agent' as const, + lastTargetType: TARGET_TYPE.AGENT, setLastTargetType: jest.fn(), controlVisibility: { accept: {isVisible: false, isEnabled: false}, @@ -139,7 +140,7 @@ describe('CallControlCAD Component', () => { setConsultAgentName: jest.fn(), holdTime: 0, startTimestamp: 0, - lastTargetType: 'agent' as const, + lastTargetType: TARGET_TYPE.AGENT, setLastTargetType: jest.fn(), controlVisibility: { accept: {isVisible: false, isEnabled: false}, @@ -217,7 +218,7 @@ describe('CallControlCAD Component', () => { setConsultAgentName: jest.fn(), holdTime: 0, startTimestamp: 0, - lastTargetType: 'agent' as const, + lastTargetType: TARGET_TYPE.AGENT, setLastTargetType: jest.fn(), controlVisibility: { accept: {isVisible: false, isEnabled: false}, @@ -297,7 +298,7 @@ describe('CallControlCAD Component', () => { setConsultAgentName: jest.fn(), holdTime: 0, startTimestamp: 0, - lastTargetType: 'agent' as const, + lastTargetType: TARGET_TYPE.AGENT, setLastTargetType: jest.fn(), controlVisibility: { accept: {isVisible: false, isEnabled: false}, diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index 67bf6cf5d..0087026ee 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -1721,10 +1721,13 @@ describe('useCallControl', () => { }); // Verify the logger was called with the correct message - expect(mockLogger.info).toHaveBeenCalledWith('Consulting agent detected: Jane Consultant consultAgentId', { - module: 'widget-cc-task#helper.ts', - method: 'useCallControl#extractConsultingAgent', - }); + expect(mockLogger.info).toHaveBeenCalledWith( + 'Consulting agent detected (fallback): Jane Consultant consultAgentId', + { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + } + ); }); it('should extract consulting agent information correctly when receiving consult', async () => { @@ -1790,10 +1793,13 @@ describe('useCallControl', () => { }); // Verify the logger was called with the correct message - expect(mockLogger.info).toHaveBeenCalledWith('Consulting agent detected: Jane Consultant consultAgentId', { - module: 'widget-cc-task#helper.ts', - method: 'useCallControl#extractConsultingAgent', - }); + expect(mockLogger.info).toHaveBeenCalledWith( + 'Consulting agent detected (fallback): Jane Consultant consultAgentId', + { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + } + ); }); it('should not update consultAgentName when no consulting agent is found', async () => { @@ -4135,6 +4141,455 @@ describe('useCallControl', () => { }); }); }); + + describe('Agent Extraction from Consult Media', () => { + const mockControlVisibility = { + accept: {isVisible: false, isEnabled: false}, + decline: {isVisible: false, isEnabled: false}, + end: {isVisible: true, isEnabled: true}, + muteUnmute: {isVisible: true, isEnabled: true}, + holdResume: {isVisible: true, isEnabled: true}, + pauseResumeRecording: {isVisible: false, isEnabled: false}, + recordingIndicator: {isVisible: false, isEnabled: false}, + transfer: {isVisible: true, isEnabled: false}, + conference: {isVisible: true, isEnabled: true}, + exitConference: {isVisible: false, isEnabled: false}, + mergeConference: {isVisible: false, isEnabled: false}, + consult: {isVisible: true, isEnabled: true}, + endConsult: {isVisible: false, isEnabled: false}, + consultTransfer: {isVisible: false, isEnabled: false}, + consultTransferConsult: {isVisible: false, isEnabled: false}, + mergeConferenceConsult: {isVisible: false, isEnabled: false}, + muteUnmuteConsult: {isVisible: false, isEnabled: false}, + switchToMainCall: {isVisible: false, isEnabled: false}, + switchToConsult: {isVisible: false, isEnabled: false}, + wrapup: {isVisible: false, isEnabled: true}, + isConferenceInProgress: false, + isConsultInitiated: false, + isConsultInitiatedAndAccepted: false, + isConsultReceived: false, + isConsultInitiatedOrAccepted: false, + isHeld: false, + consultCallHeld: false, + }; + + const mockGetControlsVisibility = jest.fn().mockReturnValue(mockControlVisibility); + + beforeEach(() => { + jest.mock('../src/Utils/task-util', () => ({ + ...jest.requireActual('../src/Utils/task-util'), + getControlsVisibility: mockGetControlsVisibility, + })); + }); + + it('should identify consulting agent from consult media participants', async () => { + const mockStoreCC = { + ...mockCC, + agentConfig: { + ...mockCC.agentConfig, + agentId: 'currentAgentId', + }, + }; + jest.spyOn(store, 'cc', 'get').mockReturnValue(mockStoreCC); + + const taskWithConsultMedia = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interactionId: 'consult-interaction', + interaction: { + ...mockCurrentTask.data.interaction, + media: { + main: { + mediaResourceId: 'main', + mType: 'telephony', + isHold: false, + participants: ['currentAgentId', 'customer1'], + }, + consult: { + mediaResourceId: 'consult', + mType: 'consult', + isHold: false, + participants: ['currentAgentId', 'consultAgentId'], + }, + }, + participants: { + currentAgentId: { + id: 'currentAgentId', + name: 'Current Agent', + pType: 'Agent', + }, + consultAgentId: { + id: 'consultAgentId', + name: 'Media Based Agent', + pType: 'Agent', + }, + otherAgentId: { + id: 'otherAgentId', + name: 'Other Agent', + pType: 'Agent', + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + }, + }, + }, + }, + on: jest.fn(), + off: jest.fn(), + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskWithConsultMedia, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + isMuted: false, + conferenceEnabled: true, + agentId: 'currentAgentId', + }) + ); + + await waitFor(() => { + // Should select the agent from consult media, not "Other Agent" + expect(result.current.consultAgentName).toBe('Media Based Agent'); + }); + + expect(mockLogger.info).toHaveBeenCalledWith('Consulting agent detected: Media Based Agent consultAgentId', { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + }); + }); + + it('should display phone number for ringing Entry Point consult', async () => { + const mockStoreCC = { + ...mockCC, + agentConfig: { + ...mockCC.agentConfig, + agentId: 'currentAgentId', + }, + }; + jest.spyOn(store, 'cc', 'get').mockReturnValue(mockStoreCC); + + const taskWithEPConsultRinging = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interactionId: 'ep-consult-ringing', + interaction: { + ...mockCurrentTask.data.interaction, + media: { + main: { + mediaResourceId: 'main', + mType: 'telephony', + isHold: false, + participants: ['currentAgentId', 'customer1'], + }, + consult: { + mediaResourceId: 'consult', + mType: 'consult', + isHold: false, + participants: ['currentAgentId', 'epParticipant'], + }, + }, + participants: { + currentAgentId: { + id: 'currentAgentId', + name: 'Current Agent', + pType: 'Agent', + }, + epParticipant: { + id: 'epParticipant', + dn: '+1234567890', + pType: 'EP', + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + }, + }, + }, + }, + on: jest.fn(), + off: jest.fn(), + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskWithEPConsultRinging, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + isMuted: false, + conferenceEnabled: true, + agentId: 'currentAgentId', + }) + ); + + // First set the target type to entryPoint + act(() => { + result.current.setLastTargetType('entryPoint'); + }); + + await waitFor(() => { + // Should show the phone number while ringing + expect(result.current.consultAgentName).toBe('+1234567890'); + }); + + expect(mockLogger.info).toHaveBeenCalledWith('entryPoint consult ringing - showing phone number: +1234567890', { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + }); + }); + + it('should display destination agent name when Entry Point consult is answered', async () => { + const mockStoreCC = { + ...mockCC, + agentConfig: { + ...mockCC.agentConfig, + agentId: 'currentAgentId', + }, + }; + jest.spyOn(store, 'cc', 'get').mockReturnValue(mockStoreCC); + + const taskWithEPConsultAnswered = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interactionId: 'ep-consult-answered', + interaction: { + ...mockCurrentTask.data.interaction, + callProcessingDetails: { + consultDestinationAgentName: 'Support Agent', + }, + media: { + main: { + mediaResourceId: 'main', + mType: 'telephony', + isHold: false, + participants: ['currentAgentId', 'customer1'], + }, + consult: { + mediaResourceId: 'consult', + mType: 'consult', + isHold: false, + participants: ['currentAgentId', 'supportAgentId'], + }, + }, + participants: { + currentAgentId: { + id: 'currentAgentId', + name: 'Current Agent', + pType: 'Agent', + }, + supportAgentId: { + id: 'supportAgentId', + name: 'Support Agent', + pType: 'Agent', + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + }, + }, + }, + }, + on: jest.fn(), + off: jest.fn(), + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskWithEPConsultAnswered, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + isMuted: false, + conferenceEnabled: true, + agentId: 'currentAgentId', + }) + ); + + // First set the target type to dialNumber + act(() => { + result.current.setLastTargetType('dialNumber'); + }); + + await waitFor(() => { + // Should show the destination agent name once answered + expect(result.current.consultAgentName).toBe('Support Agent'); + }); + + expect(mockLogger.info).toHaveBeenCalledWith('dialNumber consult answered - showing agent name: Support Agent', { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + }); + }); + + it('should use participant-based logic when consult media is unavailable', async () => { + const mockStoreCC = { + ...mockCC, + agentConfig: { + ...mockCC.agentConfig, + agentId: 'currentAgentId', + }, + }; + jest.spyOn(store, 'cc', 'get').mockReturnValue(mockStoreCC); + + const taskWithoutConsultMedia = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interactionId: 'no-consult-media', + interaction: { + ...mockCurrentTask.data.interaction, + media: { + main: { + mediaResourceId: 'main', + mType: 'telephony', + isHold: false, + participants: ['currentAgentId', 'customer1'], + }, + }, + participants: { + currentAgentId: { + id: 'currentAgentId', + name: 'Current Agent', + pType: 'Agent', + }, + consultAgentId: { + id: 'consultAgentId', + name: 'Fallback Agent', + pType: 'Agent', + consultState: 'consulting', + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + }, + }, + }, + }, + on: jest.fn(), + off: jest.fn(), + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskWithoutConsultMedia, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + isMuted: false, + conferenceEnabled: true, + agentId: 'currentAgentId', + }) + ); + + await waitFor(() => { + // Should use fallback logic and select agent with consultState="consulting" + expect(result.current.consultAgentName).toBe('Fallback Agent'); + }); + + expect(mockLogger.info).toHaveBeenCalledWith( + 'Consulting agent detected (fallback): Fallback Agent consultAgentId', + { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + } + ); + }); + + it('should prioritize consult media over timestamp-based selection in multi-agent conference', async () => { + const mockStoreCC = { + ...mockCC, + agentConfig: { + ...mockCC.agentConfig, + agentId: 'currentAgentId', + }, + }; + jest.spyOn(store, 'cc', 'get').mockReturnValue(mockStoreCC); + + const taskWithMultipleAgents = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interactionId: 'multi-agent-consult', + interaction: { + ...mockCurrentTask.data.interaction, + media: { + main: { + mediaResourceId: 'main', + mType: 'telephony', + isHold: false, + participants: ['currentAgentId', 'agent2', 'customer1'], + }, + consult: { + mediaResourceId: 'consult', + mType: 'consult', + isHold: false, + participants: ['currentAgentId', 'agent3'], + }, + }, + participants: { + currentAgentId: { + id: 'currentAgentId', + name: 'Current Agent', + pType: 'Agent', + }, + agent2: { + id: 'agent2', + name: 'Conference Agent', + pType: 'Agent', + consultTimestamp: 5000, // Higher timestamp + }, + agent3: { + id: 'agent3', + name: 'Actual Consult Agent', + pType: 'Agent', + consultTimestamp: 3000, // Lower timestamp + }, + customer1: { + id: 'customer1', + name: 'Customer', + pType: 'Customer', + }, + }, + }, + }, + on: jest.fn(), + off: jest.fn(), + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskWithMultipleAgents, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + isMuted: false, + conferenceEnabled: true, + agentId: 'currentAgentId', + }) + ); + + await waitFor(() => { + // Should select agent3 from consult media, not agent2 despite higher timestamp + expect(result.current.consultAgentName).toBe('Actual Consult Agent'); + }); + + expect(mockLogger.info).toHaveBeenCalledWith('Consulting agent detected: Actual Consult Agent agent3', { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#extractConsultingAgent', + }); + }); + }); }); describe('useOutdialCall', () => {