@webex/contact-center 3.9.0-next.25 → 3.9.0-next.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,81 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+ import {ITask} from './types';
3
+
4
+ /**
5
+ * Determines if the given agent is the primary agent (owner) of the task
6
+ * @param task - The task to check
7
+ * @param agentId - The agent ID to check for primary status
8
+ * @returns true if the agent is the primary agent, false otherwise
9
+ */
10
+ export const isPrimary = (task: ITask, agentId: string): boolean => {
11
+ if (!task.data?.interaction?.owner) {
12
+ // Fall back to checking data.agentId when owner is not set
13
+ return task.data.agentId === agentId;
14
+ }
15
+
16
+ return task.data.interaction.owner === agentId;
17
+ };
18
+
19
+ /**
20
+ * Checks if the given agent is a participant in the main interaction (mainCall)
21
+ * @param task - The task to check
22
+ * @param agentId - The agent ID to check for participation
23
+ * @returns true if the agent is a participant in the main interaction, false otherwise
24
+ */
25
+ export const isParticipantInMainInteraction = (task: ITask, agentId: string): boolean => {
26
+ if (!task?.data?.interaction?.media) {
27
+ return false;
28
+ }
29
+
30
+ return Object.values(task.data.interaction.media).some(
31
+ (mediaObj) =>
32
+ mediaObj && mediaObj.mType === 'mainCall' && mediaObj.participants?.includes(agentId)
33
+ );
34
+ };
35
+
36
+ /**
37
+ * Checks if the given agent is not in the interaction or has left the interaction
38
+ * @param task - The task to check
39
+ * @param agentId - The agent ID to check
40
+ * @returns true if the agent is not in the interaction or has left, false otherwise
41
+ */
42
+ export const checkParticipantNotInInteraction = (task: ITask, agentId: string): boolean => {
43
+ if (!task?.data?.interaction?.participants) {
44
+ return true;
45
+ }
46
+ const {data} = task;
47
+
48
+ return (
49
+ !(agentId in data.interaction.participants) ||
50
+ (agentId in data.interaction.participants && data.interaction.participants[agentId].hasLeft)
51
+ );
52
+ };
53
+
54
+ /**
55
+ * Determines if a conference is currently in progress based on the number of active agent participants
56
+ * @param task - The task to check for conference status
57
+ * @returns true if there are 2 or more active agent participants in the main call, false otherwise
58
+ */
59
+ export const getIsConferenceInProgress = (task: ITask): boolean => {
60
+ const mediaMainCall = task?.data?.interaction?.media?.[task?.data?.interactionId];
61
+ const participantsInMainCall = new Set(mediaMainCall?.participants);
62
+ const participants = task?.data?.interaction?.participants;
63
+
64
+ const agentParticipants = new Set();
65
+ if (participantsInMainCall.size > 0) {
66
+ participantsInMainCall.forEach((participantId: string) => {
67
+ const participant = participants?.[participantId];
68
+ if (
69
+ participant &&
70
+ participant.pType !== 'Customer' &&
71
+ participant.pType !== 'Supervisor' &&
72
+ !participant.hasLeft &&
73
+ participant.pType !== 'VVA'
74
+ ) {
75
+ agentParticipants.add(participantId);
76
+ }
77
+ });
78
+ }
79
+
80
+ return agentParticipants.size >= 2;
81
+ };
@@ -23,6 +23,25 @@ export const CONFERENCE_TRANSFER = '/conference/transfer';
23
23
  export const TASK_MANAGER_FILE = 'taskManager';
24
24
  export const TASK_FILE = 'task';
25
25
 
26
+ /**
27
+ * Task data field names that should be preserved during reconciliation
28
+ * These fields are retained even if not present in new data during updates
29
+ */
30
+ export const PRESERVED_TASK_DATA_FIELDS = {
31
+ /** Indicates if the task is in consultation state */
32
+ IS_CONSULTED: 'isConsulted',
33
+ /** Indicates if wrap-up is required for this task */
34
+ WRAP_UP_REQUIRED: 'wrapUpRequired',
35
+ /** Indicates if a conference is currently in progress (2+ active agents) */
36
+ IS_CONFERENCE_IN_PROGRESS: 'isConferenceInProgress',
37
+ };
38
+
39
+ /**
40
+ * Array of task data field names that should not be deleted during reconciliation
41
+ * Used by reconcileData method to preserve important task state fields
42
+ */
43
+ export const KEYS_TO_NOT_DELETE: string[] = Object.values(PRESERVED_TASK_DATA_FIELDS);
44
+
26
45
  // METHOD NAMES
27
46
  export const METHODS = {
28
47
  // Task class methods
@@ -10,7 +10,7 @@ import {
10
10
  import {Failure} from '../core/GlobalTypes';
11
11
  import {LoginOption} from '../../types';
12
12
  import {TASK_FILE} from '../../constants';
13
- import {METHODS} from './constants';
13
+ import {METHODS, KEYS_TO_NOT_DELETE} from './constants';
14
14
  import routingContact from './contact';
15
15
  import LoggerProxy from '../../logger-proxy';
16
16
  import {
@@ -281,9 +281,24 @@ export default class Task extends EventEmitter implements ITask {
281
281
  * @private
282
282
  */
283
283
  private reconcileData(oldData: TaskData, newData: TaskData): TaskData {
284
+ // Remove keys from oldData that are not in newData
285
+ Object.keys(oldData).forEach((key) => {
286
+ if (!(key in newData) && !KEYS_TO_NOT_DELETE.includes(key as string)) {
287
+ delete oldData[key];
288
+ }
289
+ });
290
+
291
+ // Merge or update keys from newData
284
292
  Object.keys(newData).forEach((key) => {
285
- if (newData[key] && typeof newData[key] === 'object' && !Array.isArray(newData[key])) {
286
- oldData[key] = this.reconcileData({...oldData[key]}, newData[key]);
293
+ if (
294
+ newData[key] &&
295
+ typeof newData[key] === 'object' &&
296
+ !Array.isArray(newData[key]) &&
297
+ oldData[key] &&
298
+ typeof oldData[key] === 'object' &&
299
+ !Array.isArray(oldData[key])
300
+ ) {
301
+ this.reconcileData(oldData[key], newData[key]);
287
302
  } else {
288
303
  oldData[key] = newData[key];
289
304
  }
@@ -1684,9 +1699,6 @@ export default class Task extends EventEmitter implements ITask {
1684
1699
  }
1685
1700
  }
1686
1701
 
1687
- // TODO: Uncomment this method in future PR for Multi-Party Conference support (>3 participants)
1688
- // Conference transfer will be supported when implementing enhanced multi-party conference functionality
1689
- /*
1690
1702
  /**
1691
1703
  * Transfers the current conference to another agent
1692
1704
  *
@@ -1707,7 +1719,7 @@ export default class Task extends EventEmitter implements ITask {
1707
1719
  * }
1708
1720
  * ```
1709
1721
  */
1710
- /* public async transferConference(): Promise<TaskResponse> {
1722
+ public async transferConference(): Promise<TaskResponse> {
1711
1723
  try {
1712
1724
  LoggerProxy.info(`Transferring conference`, {
1713
1725
  module: TASK_FILE,
@@ -1771,5 +1783,5 @@ export default class Task extends EventEmitter implements ITask {
1771
1783
 
1772
1784
  throw err;
1773
1785
  }
1774
- } */
1786
+ }
1775
1787
  }
@@ -733,6 +733,8 @@ export type TaskData = {
733
733
  isConsulted?: boolean;
734
734
  /** Indicates if the task is in conference state */
735
735
  isConferencing: boolean;
736
+ /** Indicates if a conference is currently in progress (2+ active agents) */
737
+ isConferenceInProgress?: boolean;
736
738
  /** Identifier of agent who last updated the task */
737
739
  updatedBy?: string;
738
740
  /** Type of destination for transfer/consult */
@@ -1135,16 +1137,16 @@ export interface ITask extends EventEmitter {
1135
1137
  autoWrapup?: AutoWrapup;
1136
1138
 
1137
1139
  /**
1138
- * cancels the auto-wrapup timer for the task
1139
- * This method stops the auto-wrapup process if it is currently active
1140
+ * Cancels the auto-wrapup timer for the task.
1141
+ * This method stops the auto-wrapup process if it is currently active.
1140
1142
  * Note: This is supported only in single session mode. Not supported in multi-session mode.
1141
1143
  * @returns void
1142
1144
  */
1143
1145
  cancelAutoWrapupTimer(): void;
1144
1146
 
1145
1147
  /**
1146
- * Deregisters all web call event listeners
1147
- * Used when cleaning up task resources
1148
+ * Deregisters all web call event listeners.
1149
+ * Used when cleaning up task resources.
1148
1150
  * @ignore
1149
1151
  */
1150
1152
  unregisterWebCallListeners(): void;
@@ -1164,7 +1166,7 @@ export interface ITask extends EventEmitter {
1164
1166
  * @returns Promise<TaskResponse>
1165
1167
  * @example
1166
1168
  * ```typescript
1167
- * task.accept();
1169
+ * await task.accept();
1168
1170
  * ```
1169
1171
  */
1170
1172
  accept(): Promise<TaskResponse>;
@@ -1174,48 +1176,48 @@ export interface ITask extends EventEmitter {
1174
1176
  * @returns Promise<TaskResponse>
1175
1177
  * @example
1176
1178
  * ```typescript
1177
- * task.decline();
1179
+ * await task.decline();
1178
1180
  * ```
1179
1181
  */
1180
1182
  decline(): Promise<TaskResponse>;
1181
1183
 
1182
1184
  /**
1183
- * Places the current task on hold
1185
+ * Places the current task on hold.
1184
1186
  * @returns Promise<TaskResponse>
1185
1187
  * @example
1186
1188
  * ```typescript
1187
- * task.hold();
1189
+ * await task.hold();
1188
1190
  * ```
1189
1191
  */
1190
1192
  hold(): Promise<TaskResponse>;
1191
1193
 
1192
1194
  /**
1193
- * Resumes a task that was previously on hold
1195
+ * Resumes a task that was previously on hold.
1194
1196
  * @returns Promise<TaskResponse>
1195
1197
  * @example
1196
1198
  * ```typescript
1197
- * task.resume();
1199
+ * await task.resume();
1198
1200
  * ```
1199
1201
  */
1200
1202
  resume(): Promise<TaskResponse>;
1201
1203
 
1202
1204
  /**
1203
- * Ends/terminates the current task
1205
+ * Ends/terminates the current task.
1204
1206
  * @returns Promise<TaskResponse>
1205
1207
  * @example
1206
1208
  * ```typescript
1207
- * task.end();
1209
+ * await task.end();
1208
1210
  * ```
1209
1211
  */
1210
1212
  end(): Promise<TaskResponse>;
1211
1213
 
1212
1214
  /**
1213
- * Initiates wrap-up process for the task with specified details
1215
+ * Initiates wrap-up process for the task with specified details.
1214
1216
  * @param wrapupPayload - Wrap-up details including reason and auxiliary code
1215
1217
  * @returns Promise<TaskResponse>
1216
1218
  * @example
1217
1219
  * ```typescript
1218
- * task.wrapup({
1220
+ * await task.wrapup({
1219
1221
  * wrapUpReason: "Customer issue resolved",
1220
1222
  * auxCodeId: "RESOLVED"
1221
1223
  * });
@@ -1224,25 +1226,109 @@ export interface ITask extends EventEmitter {
1224
1226
  wrapup(wrapupPayload: WrapupPayLoad): Promise<TaskResponse>;
1225
1227
 
1226
1228
  /**
1227
- * Pauses the recording for current task
1229
+ * Pauses the recording for current task.
1228
1230
  * @returns Promise<TaskResponse>
1229
1231
  * @example
1230
1232
  * ```typescript
1231
- * task.pauseRecording();
1233
+ * await task.pauseRecording();
1232
1234
  * ```
1233
1235
  */
1234
1236
  pauseRecording(): Promise<TaskResponse>;
1235
1237
 
1236
1238
  /**
1237
- * Resumes a previously paused recording
1239
+ * Resumes a previously paused recording.
1238
1240
  * @param resumeRecordingPayload - Parameters for resuming the recording
1239
1241
  * @returns Promise<TaskResponse>
1240
1242
  * @example
1241
1243
  * ```typescript
1242
- * task.resumeRecording({
1244
+ * await task.resumeRecording({
1243
1245
  * autoResumed: false
1244
1246
  * });
1245
1247
  * ```
1246
1248
  */
1247
1249
  resumeRecording(resumeRecordingPayload: ResumeRecordingPayload): Promise<TaskResponse>;
1250
+
1251
+ /**
1252
+ * Initiates a consultation with another agent or queue.
1253
+ * @param consultPayload - Consultation details including destination and type
1254
+ * @returns Promise<TaskResponse>
1255
+ * @example
1256
+ * ```typescript
1257
+ * await task.consult({ to: "agentId", destinationType: "agent" });
1258
+ * ```
1259
+ */
1260
+ consult(consultPayload: ConsultPayload): Promise<TaskResponse>;
1261
+
1262
+ /**
1263
+ * Ends an ongoing consultation.
1264
+ * @param consultEndPayload - Details for ending the consultation
1265
+ * @returns Promise<TaskResponse>
1266
+ * @example
1267
+ * ```typescript
1268
+ * await task.endConsult({ isConsult: true, taskId: "taskId" });
1269
+ * ```
1270
+ */
1271
+ endConsult(consultEndPayload: ConsultEndPayload): Promise<TaskResponse>;
1272
+
1273
+ /**
1274
+ * Transfers the task to another agent or queue.
1275
+ * @param transferPayload - Transfer details including destination and type
1276
+ * @returns Promise<TaskResponse>
1277
+ * @example
1278
+ * ```typescript
1279
+ * await task.transfer({ to: "queueId", destinationType: "queue" });
1280
+ * ```
1281
+ */
1282
+ transfer(transferPayload: TransferPayLoad): Promise<TaskResponse>;
1283
+
1284
+ /**
1285
+ * Transfers the task after consultation.
1286
+ * @param consultTransferPayload - Details for consult transfer (optional)
1287
+ * @returns Promise<TaskResponse>
1288
+ * @example
1289
+ * ```typescript
1290
+ * await task.consultTransfer({ to: "agentId", destinationType: "agent" });
1291
+ * ```
1292
+ */
1293
+ consultTransfer(consultTransferPayload?: ConsultTransferPayLoad): Promise<TaskResponse>;
1294
+
1295
+ /**
1296
+ * Initiates a consult conference (merge consult call with main call).
1297
+ * @returns Promise<TaskResponse>
1298
+ * @example
1299
+ * ```typescript
1300
+ * await task.consultConference();
1301
+ * ```
1302
+ */
1303
+ consultConference(): Promise<TaskResponse>;
1304
+
1305
+ /**
1306
+ * Exits from an ongoing conference.
1307
+ * @returns Promise<TaskResponse>
1308
+ * @example
1309
+ * ```typescript
1310
+ * await task.exitConference();
1311
+ * ```
1312
+ */
1313
+ exitConference(): Promise<TaskResponse>;
1314
+
1315
+ /**
1316
+ * Transfers the conference to another participant.
1317
+ * @returns Promise<TaskResponse>
1318
+ * @example
1319
+ * ```typescript
1320
+ * await task.transferConference();
1321
+ * ```
1322
+ */
1323
+ transferConference(): Promise<TaskResponse>;
1324
+
1325
+ /**
1326
+ * Toggles mute/unmute for the local audio stream during a WebRTC task.
1327
+ * @returns Promise<void>
1328
+ * @example
1329
+ * ```typescript
1330
+ * await task.toggleMute();
1331
+ * ```
1332
+ */
1333
+ toggleMute(): Promise<void>;
1248
1334
  }
@@ -1357,8 +1357,12 @@ describe('TaskManager', () => {
1357
1357
 
1358
1358
  describe('Conference event handling', () => {
1359
1359
  let task;
1360
+ const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1360
1361
 
1361
1362
  beforeEach(() => {
1363
+ // Set the agentId on taskManager before tests run
1364
+ taskManager.setAgentId(agentId);
1365
+
1362
1366
  task = {
1363
1367
  data: { interactionId: taskId },
1364
1368
  emit: jest.fn(),
@@ -1434,19 +1438,260 @@ describe('TaskManager', () => {
1434
1438
  // No specific task event emission for participant joined - just data update
1435
1439
  });
1436
1440
 
1437
- it('should handle PARTICIPANT_LEFT_CONFERENCE event', () => {
1438
- const payload = {
1439
- data: {
1440
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1441
- interactionId: taskId,
1442
- isConferencing: false,
1443
- },
1444
- };
1441
+ describe('PARTICIPANT_LEFT_CONFERENCE event handling', () => {
1442
+ it('should emit TASK_PARTICIPANT_LEFT event when participant leaves conference', () => {
1443
+ const payload = {
1444
+ data: {
1445
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1446
+ interactionId: taskId,
1447
+ interaction: {
1448
+ participants: {
1449
+ [agentId]: {
1450
+ hasLeft: false,
1451
+ },
1452
+ },
1453
+ },
1454
+ },
1455
+ };
1445
1456
 
1446
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1457
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1458
+
1459
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1460
+ });
1461
+
1462
+ it('should NOT remove task when agent is still in interaction', () => {
1463
+ const payload = {
1464
+ data: {
1465
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1466
+ interactionId: taskId,
1467
+ interaction: {
1468
+ participants: {
1469
+ [agentId]: {
1470
+ hasLeft: false,
1471
+ },
1472
+ },
1473
+ },
1474
+ },
1475
+ };
1476
+
1477
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1447
1478
 
1448
- expect(task.data.isConferencing).toBe(false);
1449
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1479
+ // Task should still exist in collection
1480
+ expect(taskManager.getTask(taskId)).toBeDefined();
1481
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1482
+ });
1483
+
1484
+ it('should NOT remove task when agent left but is in main interaction', () => {
1485
+ const payload = {
1486
+ data: {
1487
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1488
+ interactionId: taskId,
1489
+ interaction: {
1490
+ participants: {
1491
+ [agentId]: {
1492
+ hasLeft: true,
1493
+ },
1494
+ },
1495
+ media: {
1496
+ [taskId]: {
1497
+ mType: 'mainCall',
1498
+ participants: [agentId],
1499
+ },
1500
+ },
1501
+ },
1502
+ },
1503
+ };
1504
+
1505
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1506
+
1507
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1508
+
1509
+ // Task should still exist - not removed
1510
+ expect(removeTaskSpy).not.toHaveBeenCalled();
1511
+ expect(taskManager.getTask(taskId)).toBeDefined();
1512
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1513
+ });
1514
+
1515
+ it('should NOT remove task when agent left but is primary (owner)', () => {
1516
+ const payload = {
1517
+ data: {
1518
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1519
+ interactionId: taskId,
1520
+ interaction: {
1521
+ participants: {
1522
+ [agentId]: {
1523
+ hasLeft: true,
1524
+ },
1525
+ },
1526
+ owner: agentId,
1527
+ media: {
1528
+ [taskId]: {
1529
+ mType: 'consultCall',
1530
+ participants: ['other-agent'],
1531
+ },
1532
+ },
1533
+ },
1534
+ },
1535
+ };
1536
+
1537
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1538
+
1539
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1540
+
1541
+ // Task should still exist - not removed because agent is primary
1542
+ expect(removeTaskSpy).not.toHaveBeenCalled();
1543
+ expect(taskManager.getTask(taskId)).toBeDefined();
1544
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1545
+ });
1546
+
1547
+ it('should remove task when agent left and is NOT in main interaction and is NOT primary', () => {
1548
+ const payload = {
1549
+ data: {
1550
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1551
+ interactionId: taskId,
1552
+ interaction: {
1553
+ participants: {
1554
+ [agentId]: {
1555
+ hasLeft: true,
1556
+ },
1557
+ },
1558
+ owner: 'another-agent-id',
1559
+ media: {
1560
+ [taskId]: {
1561
+ mType: 'mainCall',
1562
+ participants: ['another-agent-id'],
1563
+ },
1564
+ },
1565
+ },
1566
+ },
1567
+ };
1568
+
1569
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1570
+
1571
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1572
+
1573
+ // Task should be removed
1574
+ expect(removeTaskSpy).toHaveBeenCalled();
1575
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1576
+ });
1577
+
1578
+ it('should remove task when agent is not in participants list', () => {
1579
+ const payload = {
1580
+ data: {
1581
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1582
+ interactionId: taskId,
1583
+ interaction: {
1584
+ participants: {
1585
+ 'other-agent-id': {
1586
+ hasLeft: false,
1587
+ },
1588
+ },
1589
+ owner: 'another-agent-id',
1590
+ },
1591
+ },
1592
+ };
1593
+
1594
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1595
+
1596
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1597
+
1598
+ // Task should be removed because agent is not in participants
1599
+ expect(removeTaskSpy).toHaveBeenCalled();
1600
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1601
+ });
1602
+
1603
+ it('should update isConferenceInProgress based on remaining active agents', () => {
1604
+ const payload = {
1605
+ data: {
1606
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1607
+ interactionId: taskId,
1608
+ interaction: {
1609
+ participants: {
1610
+ [agentId]: {
1611
+ hasLeft: false,
1612
+ pType: 'Agent',
1613
+ },
1614
+ 'agent-2': {
1615
+ hasLeft: false,
1616
+ pType: 'Agent',
1617
+ },
1618
+ 'customer-1': {
1619
+ hasLeft: false,
1620
+ pType: 'Customer',
1621
+ },
1622
+ },
1623
+ media: {
1624
+ [taskId]: {
1625
+ mType: 'mainCall',
1626
+ participants: [agentId, 'agent-2', 'customer-1'],
1627
+ },
1628
+ },
1629
+ },
1630
+ },
1631
+ };
1632
+
1633
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1634
+
1635
+ // isConferenceInProgress should be true (2 active agents)
1636
+ expect(task.data.isConferenceInProgress).toBe(true);
1637
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1638
+ });
1639
+
1640
+ it('should set isConferenceInProgress to false when only one agent remains', () => {
1641
+ const payload = {
1642
+ data: {
1643
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1644
+ interactionId: taskId,
1645
+ interaction: {
1646
+ participants: {
1647
+ [agentId]: {
1648
+ hasLeft: false,
1649
+ pType: 'Agent',
1650
+ },
1651
+ 'agent-2': {
1652
+ hasLeft: true,
1653
+ pType: 'Agent',
1654
+ },
1655
+ 'customer-1': {
1656
+ hasLeft: false,
1657
+ pType: 'Customer',
1658
+ },
1659
+ },
1660
+ media: {
1661
+ [taskId]: {
1662
+ mType: 'mainCall',
1663
+ participants: [agentId, 'customer-1'],
1664
+ },
1665
+ },
1666
+ },
1667
+ },
1668
+ };
1669
+
1670
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1671
+
1672
+ // isConferenceInProgress should be false (only 1 active agent)
1673
+ expect(task.data.isConferenceInProgress).toBe(false);
1674
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1675
+ });
1676
+
1677
+ it('should handle participant left when no participants data exists', () => {
1678
+ const payload = {
1679
+ data: {
1680
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1681
+ interactionId: taskId,
1682
+ interaction: {},
1683
+ },
1684
+ };
1685
+
1686
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1687
+
1688
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1689
+
1690
+ // When no participants data exists, checkParticipantNotInInteraction returns true
1691
+ // Since agent won't be in main interaction either, task should be removed
1692
+ expect(removeTaskSpy).toHaveBeenCalled();
1693
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1694
+ });
1450
1695
  });
1451
1696
 
1452
1697
  it('should handle PARTICIPANT_LEFT_CONFERENCE_FAILED event', () => {