Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,20 @@ export default class TaskManager extends EventEmitter {
method: METHODS.REGISTER_TASK_LISTENERS,
interactionId: payload.data.interactionId,
});

// Pre-calculate isConferenceInProgress for the initial task data
const simulatedTaskForAgentContact = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this constant? Can we not use payload.data directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because isConferenceInProgress is taking task object as an parameter, so we needed an updated task object. We can pass payload data, but we wanted to keep it consistent with all other utils(task object as parameter)

data: {...payload.data},
} as ITask;

task = new Task(
this.contact,
this.webCallingService,
{
...payload.data,
wrapUpRequired:
payload.data.interaction?.participants?.[this.agentId]?.isWrapUp || false,
isConferenceInProgress: getIsConferenceInProgress(simulatedTaskForAgentContact),
},
this.wrapupData,
this.agentId
Expand Down Expand Up @@ -165,6 +172,7 @@ export default class TaskManager extends EventEmitter {
}
}
break;

case CC_EVENTS.AGENT_CONTACT_RESERVED:
task = new Task(
this.contact,
Expand Down
40 changes: 31 additions & 9 deletions packages/@webex/contact-center/src/services/task/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1556,14 +1556,24 @@ export default class Task extends EventEmitter implements ITask {
* ```
*/
public async consultConference(): Promise<TaskResponse> {
// Extract consultation conference data from task data (used in both try and catch)
const consultationData = {
agentId: this.agentId,
destAgentId: this.data.destAgentId,
destinationType: this.data.destinationType || 'agent',
};

try {
// Get the destination agent ID using custom logic from participants data (same as consultTransfer)
const destAgentId = getDestinationAgentId(
this.data.interaction?.participants,
this.data.agentId
);

// Validate that we have a destination agent (for queue consult scenarios)
if (!destAgentId) {
throw new Error('No agent has accepted this queue consult yet');
}
// Extract consultation conference data from task data (used in both try and catch)
const consultationData = {
agentId: this.agentId,
destAgentId,
destinationType: this.data.destinationType || 'agent',
};

LoggerProxy.info(`Initiating consult conference to ${consultationData.destAgentId}`, {
module: TASK_FILE,
method: METHODS.CONSULT_CONFERENCE,
Expand Down Expand Up @@ -1611,9 +1621,21 @@ export default class Task extends EventEmitter implements ITask {
};

// Track failure metrics (following consultTransfer pattern)
// Build conference data for error tracking using extracted data
// Recalculate destination info for error tracking
const failedDestAgentId = getDestinationAgentId(
this.data.interaction?.participants,
this.data.agentId
);

// Build conference data for error tracking using recalculated data
const failedConsultationData = {
agentId: this.agentId,
destAgentId: failedDestAgentId,
destinationType: this.data.destinationType || 'agent',
};

const failedParamsData = buildConsultConferenceParamData(
consultationData,
failedConsultationData,
this.data.interactionId
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,73 @@ describe('TaskManager', () => {
);
});

it('should set isConferenceInProgress correctly when creating task via AGENT_CONTACT with conference in progress', () => {
const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
taskManager.setAgentId(testAgentId);
taskManager.taskCollection = [];

const payload = {
data: {
...initalPayload.data,
type: CC_EVENTS.AGENT_CONTACT,
interaction: {
mediaType: 'telephony',
state: 'conference',
participants: {
[testAgentId]: { pType: 'Agent', hasLeft: false },
'agent-2': { pType: 'Agent', hasLeft: false },
'customer-1': { pType: 'Customer', hasLeft: false },
},
media: {
[taskId]: {
mType: 'mainCall',
participants: [testAgentId, 'agent-2', 'customer-1'],
},
},
},
},
};

webSocketManagerMock.emit('message', JSON.stringify(payload));

const createdTask = taskManager.getTask(taskId);
expect(createdTask).toBeDefined();
expect(createdTask.data.isConferenceInProgress).toBe(true);
});

it('should set isConferenceInProgress to false when creating task via AGENT_CONTACT with only one agent', () => {
const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
taskManager.setAgentId(testAgentId);
taskManager.taskCollection = [];

const payload = {
data: {
...initalPayload.data,
type: CC_EVENTS.AGENT_CONTACT,
interaction: {
mediaType: 'telephony',
state: 'connected',
participants: {
[testAgentId]: { pType: 'Agent', hasLeft: false },
'customer-1': { pType: 'Customer', hasLeft: false },
},
media: {
[taskId]: {
mType: 'mainCall',
participants: [testAgentId, 'customer-1'],
},
},
},
},
};

webSocketManagerMock.emit('message', JSON.stringify(payload));

const createdTask = taskManager.getTask(taskId);
expect(createdTask).toBeDefined();
expect(createdTask.data.isConferenceInProgress).toBe(false);
});

it('should emit TASK_END event on AGENT_WRAPUP event', () => {
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));

Expand Down Expand Up @@ -1438,7 +1505,86 @@ describe('TaskManager', () => {
// No specific task event emission for participant joined - just data update
});

it('should call updateTaskData only once for PARTICIPANT_JOINED_CONFERENCE with pre-calculated isConferenceInProgress', () => {
const payload = {
data: {
type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
interactionId: taskId,
participantId: 'new-agent-789',
interaction: {
participants: {
[agentId]: { pType: 'Agent', hasLeft: false },
'agent-2': { pType: 'Agent', hasLeft: false },
'new-agent-789': { pType: 'Agent', hasLeft: false },
'customer-1': { pType: 'Customer', hasLeft: false },
},
media: {
[taskId]: {
mType: 'mainCall',
participants: [agentId, 'agent-2', 'new-agent-789', 'customer-1'],
},
},
},
},
};

const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');

webSocketManagerMock.emit('message', JSON.stringify(payload));

// Verify updateTaskData was called exactly once
expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);

// Verify it was called with isConferenceInProgress already calculated
expect(updateTaskDataSpy).toHaveBeenCalledWith(
expect.objectContaining({
participantId: 'new-agent-789',
isConferenceInProgress: true, // 3 active agents
})
);

expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
});

describe('PARTICIPANT_LEFT_CONFERENCE event handling', () => {
it('should call updateTaskData only once for PARTICIPANT_LEFT_CONFERENCE with pre-calculated isConferenceInProgress', () => {
const payload = {
data: {
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
interactionId: taskId,
interaction: {
participants: {
[agentId]: { pType: 'Agent', hasLeft: false },
'agent-2': { pType: 'Agent', hasLeft: true }, // This agent left
'customer-1': { pType: 'Customer', hasLeft: false },
},
media: {
[taskId]: {
mType: 'mainCall',
participants: [agentId, 'customer-1'], // agent-2 removed from participants
},
},
},
},
};

const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');

webSocketManagerMock.emit('message', JSON.stringify(payload));

// Verify updateTaskData was called exactly once
expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);

// Verify it was called with isConferenceInProgress already calculated
expect(updateTaskDataSpy).toHaveBeenCalledWith(
expect.objectContaining({
isConferenceInProgress: false, // Only 1 active agent remains
})
);

expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
});

it('should emit TASK_PARTICIPANT_LEFT event when participant leaves conference', () => {
const payload = {
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,64 @@ describe('Task', () => {
interactionId: taskId,
});
});

it('should dynamically calculate destAgentId from participants when this.data.destAgentId is null', async () => {
// Simulate scenario where destAgentId is not preserved (e.g., after hold/unhold)
task.data.destAgentId = null;

const consultedAgentId = 'consulted-agent-123';
getDestinationAgentIdSpy.mockReturnValue(consultedAgentId);

const mockResponse = {
trackingId: 'test-tracking-dynamic',
interactionId: taskId,
};
contactMock.consultConference.mockResolvedValue(mockResponse);

const result = await task.consultConference();

// Verify getDestinationAgentId was called to dynamically calculate the destination
expect(getDestinationAgentIdSpy).toHaveBeenCalledWith(
taskDataMock.interaction?.participants,
taskDataMock.agentId
);

// Verify the conference was called with the dynamically calculated destAgentId
expect(contactMock.consultConference).toHaveBeenCalledWith({
interactionId: taskId,
data: {
agentId: taskDataMock.agentId,
to: consultedAgentId, // Dynamically calculated value
destinationType: 'agent',
},
});
expect(result).toEqual(mockResponse);
});

it('should throw error when no destination agent is found (queue consult not accepted)', async () => {
// Simulate queue consult scenario where no agent has accepted yet
getDestinationAgentIdSpy.mockReturnValue(''); // No agent found

// Mock generateTaskErrorObject to wrap the error
const wrappedError = new Error('Error while performing consultConference');
generateTaskErrorObjectSpy.mockReturnValue(wrappedError);

await expect(task.consultConference()).rejects.toThrow('Error while performing consultConference');

// Verify the conference was NOT called
expect(contactMock.consultConference).not.toHaveBeenCalled();

// Verify metrics were tracked for the failure
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
'Task Conference Start Failed',
expect.objectContaining({
taskId: taskId,
destination: '', // No destination found
destinationType: 'agent',
}),
['operational', 'behavioral', 'business']
);
});
});

describe('exitConference', () => {
Expand Down
Loading