@webex/contact-center 3.9.0 → 3.10.0-multi-llms.1

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.
Files changed (115) hide show
  1. package/dist/cc.js +207 -47
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +1 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/index.js +9 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/logger-proxy.js +24 -1
  8. package/dist/logger-proxy.js.map +1 -1
  9. package/dist/metrics/MetricsManager.js +1 -1
  10. package/dist/metrics/MetricsManager.js.map +1 -1
  11. package/dist/metrics/behavioral-events.js +89 -0
  12. package/dist/metrics/behavioral-events.js.map +1 -1
  13. package/dist/metrics/constants.js +32 -2
  14. package/dist/metrics/constants.js.map +1 -1
  15. package/dist/services/AddressBook.js +271 -0
  16. package/dist/services/AddressBook.js.map +1 -0
  17. package/dist/services/EntryPoint.js +227 -0
  18. package/dist/services/EntryPoint.js.map +1 -0
  19. package/dist/services/Queue.js +261 -0
  20. package/dist/services/Queue.js.map +1 -0
  21. package/dist/services/config/constants.js +36 -2
  22. package/dist/services/config/constants.js.map +1 -1
  23. package/dist/services/config/index.js +29 -21
  24. package/dist/services/config/index.js.map +1 -1
  25. package/dist/services/config/types.js +33 -1
  26. package/dist/services/config/types.js.map +1 -1
  27. package/dist/services/core/GlobalTypes.js.map +1 -1
  28. package/dist/services/core/Utils.js +181 -2
  29. package/dist/services/core/Utils.js.map +1 -1
  30. package/dist/services/core/aqm-reqs.js +0 -4
  31. package/dist/services/core/aqm-reqs.js.map +1 -1
  32. package/dist/services/core/constants.js +17 -1
  33. package/dist/services/core/constants.js.map +1 -1
  34. package/dist/services/core/websocket/WebSocketManager.js +0 -4
  35. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  36. package/dist/services/task/TaskManager.js +151 -7
  37. package/dist/services/task/TaskManager.js.map +1 -1
  38. package/dist/services/task/TaskUtils.js +104 -0
  39. package/dist/services/task/TaskUtils.js.map +1 -0
  40. package/dist/services/task/constants.js +26 -1
  41. package/dist/services/task/constants.js.map +1 -1
  42. package/dist/services/task/contact.js +86 -0
  43. package/dist/services/task/contact.js.map +1 -1
  44. package/dist/services/task/index.js +428 -91
  45. package/dist/services/task/index.js.map +1 -1
  46. package/dist/services/task/types.js +12 -0
  47. package/dist/services/task/types.js.map +1 -1
  48. package/dist/types/cc.d.ts +121 -35
  49. package/dist/types/constants.d.ts +1 -0
  50. package/dist/types/index.d.ts +4 -3
  51. package/dist/types/metrics/constants.d.ts +25 -1
  52. package/dist/types/services/AddressBook.d.ts +74 -0
  53. package/dist/types/services/EntryPoint.d.ts +67 -0
  54. package/dist/types/services/Queue.d.ts +76 -0
  55. package/dist/types/services/config/constants.d.ts +35 -1
  56. package/dist/types/services/config/index.d.ts +6 -9
  57. package/dist/types/services/config/types.d.ts +79 -58
  58. package/dist/types/services/core/GlobalTypes.d.ts +25 -0
  59. package/dist/types/services/core/Utils.d.ts +55 -1
  60. package/dist/types/services/core/constants.d.ts +14 -0
  61. package/dist/types/services/task/TaskUtils.d.ts +42 -0
  62. package/dist/types/services/task/constants.d.ts +23 -0
  63. package/dist/types/services/task/contact.d.ts +10 -0
  64. package/dist/types/services/task/index.d.ts +85 -4
  65. package/dist/types/services/task/types.d.ts +245 -21
  66. package/dist/types/types.d.ts +162 -0
  67. package/dist/types/utils/PageCache.d.ts +173 -0
  68. package/dist/types.js +17 -0
  69. package/dist/types.js.map +1 -1
  70. package/dist/utils/PageCache.js +192 -0
  71. package/dist/utils/PageCache.js.map +1 -0
  72. package/dist/webex.js +1 -1
  73. package/package.json +10 -9
  74. package/src/cc.ts +232 -52
  75. package/src/constants.ts +1 -0
  76. package/src/index.ts +17 -2
  77. package/src/logger-proxy.ts +24 -1
  78. package/src/metrics/MetricsManager.ts +1 -1
  79. package/src/metrics/behavioral-events.ts +94 -0
  80. package/src/metrics/constants.ts +37 -1
  81. package/src/services/AddressBook.ts +291 -0
  82. package/src/services/EntryPoint.ts +241 -0
  83. package/src/services/Queue.ts +277 -0
  84. package/src/services/config/constants.ts +42 -2
  85. package/src/services/config/index.ts +30 -30
  86. package/src/services/config/types.ts +59 -58
  87. package/src/services/core/GlobalTypes.ts +27 -0
  88. package/src/services/core/Utils.ts +215 -1
  89. package/src/services/core/aqm-reqs.ts +0 -5
  90. package/src/services/core/constants.ts +16 -0
  91. package/src/services/core/websocket/WebSocketManager.ts +0 -4
  92. package/src/services/task/TaskManager.ts +182 -9
  93. package/src/services/task/TaskUtils.ts +113 -0
  94. package/src/services/task/constants.ts +25 -0
  95. package/src/services/task/contact.ts +80 -0
  96. package/src/services/task/index.ts +497 -71
  97. package/src/services/task/types.ts +264 -20
  98. package/src/types.ts +180 -0
  99. package/src/utils/PageCache.ts +252 -0
  100. package/test/unit/spec/cc.ts +282 -85
  101. package/test/unit/spec/metrics/MetricsManager.ts +0 -1
  102. package/test/unit/spec/metrics/behavioral-events.ts +42 -0
  103. package/test/unit/spec/services/AddressBook.ts +332 -0
  104. package/test/unit/spec/services/EntryPoint.ts +259 -0
  105. package/test/unit/spec/services/Queue.ts +323 -0
  106. package/test/unit/spec/services/config/index.ts +279 -65
  107. package/test/unit/spec/services/core/Utils.ts +282 -1
  108. package/test/unit/spec/services/core/aqm-reqs.ts +1 -3
  109. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +0 -4
  110. package/test/unit/spec/services/task/TaskManager.ts +760 -2
  111. package/test/unit/spec/services/task/TaskUtils.ts +131 -0
  112. package/test/unit/spec/services/task/contact.ts +31 -1
  113. package/test/unit/spec/services/task/index.ts +873 -163
  114. package/umd/contact-center.min.js +2 -2
  115. package/umd/contact-center.min.js.map +1 -1
@@ -479,6 +479,73 @@ describe('TaskManager', () => {
479
479
  );
480
480
  });
481
481
 
482
+ it('should set isConferenceInProgress correctly when creating task via AGENT_CONTACT with conference in progress', () => {
483
+ const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
484
+ taskManager.setAgentId(testAgentId);
485
+ taskManager.taskCollection = [];
486
+
487
+ const payload = {
488
+ data: {
489
+ ...initalPayload.data,
490
+ type: CC_EVENTS.AGENT_CONTACT,
491
+ interaction: {
492
+ mediaType: 'telephony',
493
+ state: 'conference',
494
+ participants: {
495
+ [testAgentId]: { pType: 'Agent', hasLeft: false },
496
+ 'agent-2': { pType: 'Agent', hasLeft: false },
497
+ 'customer-1': { pType: 'Customer', hasLeft: false },
498
+ },
499
+ media: {
500
+ [taskId]: {
501
+ mType: 'mainCall',
502
+ participants: [testAgentId, 'agent-2', 'customer-1'],
503
+ },
504
+ },
505
+ },
506
+ },
507
+ };
508
+
509
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
510
+
511
+ const createdTask = taskManager.getTask(taskId);
512
+ expect(createdTask).toBeDefined();
513
+ expect(createdTask.data.isConferenceInProgress).toBe(true);
514
+ });
515
+
516
+ it('should set isConferenceInProgress to false when creating task via AGENT_CONTACT with only one agent', () => {
517
+ const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
518
+ taskManager.setAgentId(testAgentId);
519
+ taskManager.taskCollection = [];
520
+
521
+ const payload = {
522
+ data: {
523
+ ...initalPayload.data,
524
+ type: CC_EVENTS.AGENT_CONTACT,
525
+ interaction: {
526
+ mediaType: 'telephony',
527
+ state: 'connected',
528
+ participants: {
529
+ [testAgentId]: { pType: 'Agent', hasLeft: false },
530
+ 'customer-1': { pType: 'Customer', hasLeft: false },
531
+ },
532
+ media: {
533
+ [taskId]: {
534
+ mType: 'mainCall',
535
+ participants: [testAgentId, 'customer-1'],
536
+ },
537
+ },
538
+ },
539
+ },
540
+ };
541
+
542
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
543
+
544
+ const createdTask = taskManager.getTask(taskId);
545
+ expect(createdTask).toBeDefined();
546
+ expect(createdTask.data.isConferenceInProgress).toBe(false);
547
+ });
548
+
482
549
  it('should emit TASK_END event on AGENT_WRAPUP event', () => {
483
550
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
484
551
 
@@ -658,6 +725,13 @@ describe('TaskManager', () => {
658
725
  });
659
726
 
660
727
  it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
728
+ const initialConsultingPayload = {
729
+ data: {
730
+ ...initalPayload.data,
731
+ type: CC_EVENTS.AGENT_OFFER_CONSULT,
732
+ },
733
+ };
734
+
661
735
  const consultingPayload = {
662
736
  data: {
663
737
  ...initalPayload.data,
@@ -672,8 +746,8 @@ describe('TaskManager', () => {
672
746
  });
673
747
 
674
748
  const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
749
+ webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
675
750
  webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
676
- expect(taskManager.getTask(taskId).updateTaskData).toHaveBeenCalledWith(consultingPayload.data);
677
751
  expect(taskManager.getTask(taskId).data.isConsulted).toBe(true);
678
752
  expect(taskEmitSpy).toHaveBeenCalledWith(
679
753
  TASK_EVENTS.TASK_CONSULT_ACCEPTED,
@@ -1346,6 +1420,690 @@ describe('TaskManager', () => {
1346
1420
  expect(spy).toHaveBeenCalledWith(taskEvent, task);
1347
1421
  });
1348
1422
  });
1349
- });
1423
+ });
1424
+
1425
+ describe('Conference event handling', () => {
1426
+ let task;
1427
+ const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1428
+
1429
+ beforeEach(() => {
1430
+ // Set the agentId on taskManager before tests run
1431
+ taskManager.setAgentId(agentId);
1432
+
1433
+ task = {
1434
+ data: { interactionId: taskId },
1435
+ emit: jest.fn(),
1436
+ updateTaskData: jest.fn().mockImplementation((updatedData) => {
1437
+ // Mock the updateTaskData method to actually update task.data
1438
+ task.data = { ...task.data, ...updatedData };
1439
+ return task;
1440
+ }),
1441
+ };
1442
+ taskManager.taskCollection[taskId] = task;
1443
+ });
1444
+
1445
+ it('should handle AGENT_CONSULT_CONFERENCED event', () => {
1446
+ const payload = {
1447
+ data: {
1448
+ type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
1449
+ interactionId: taskId,
1450
+ isConferencing: true,
1451
+ },
1452
+ };
1453
+
1454
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1455
+
1456
+ expect(task.data.isConferencing).toBe(true);
1457
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
1458
+ });
1459
+
1460
+ it('should handle AGENT_CONSULT_CONFERENCING event', () => {
1461
+ const payload = {
1462
+ data: {
1463
+ type: CC_EVENTS.AGENT_CONSULT_CONFERENCING,
1464
+ interactionId: taskId,
1465
+ isConferencing: true,
1466
+ },
1467
+ };
1468
+
1469
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1470
+
1471
+ expect(task.data.isConferencing).toBe(true);
1472
+ // No task event emission for conferencing - only for conferenced (completed)
1473
+ expect(task.emit).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
1474
+ });
1475
+
1476
+ it('should handle AGENT_CONSULT_CONFERENCE_FAILED event', () => {
1477
+ const payload = {
1478
+ data: {
1479
+ type: CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED,
1480
+ interactionId: taskId,
1481
+ reason: 'Network error',
1482
+ },
1483
+ };
1484
+
1485
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1486
+
1487
+ expect(task.data.reason).toBe('Network error');
1488
+ // No event emission expected for failure - handled by contact method promise rejection
1489
+ });
1490
+
1491
+ it('should handle PARTICIPANT_JOINED_CONFERENCE event', () => {
1492
+ const payload = {
1493
+ data: {
1494
+ type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
1495
+ interactionId: taskId,
1496
+ participantId: 'new-participant-123',
1497
+ participantType: 'agent',
1498
+ interaction: {
1499
+ participants: {
1500
+ [agentId]: { pType: 'Agent', hasLeft: false },
1501
+ 'new-participant-123': { pType: 'Agent', hasLeft: false },
1502
+ },
1503
+ media: {
1504
+ [taskId]: {
1505
+ mType: 'mainCall',
1506
+ participants: [agentId, 'new-participant-123'],
1507
+ },
1508
+ },
1509
+ },
1510
+ },
1511
+ };
1512
+
1513
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1514
+
1515
+ expect(task.data.participantId).toBe('new-participant-123');
1516
+ expect(task.data.participantType).toBe('agent');
1517
+ // No specific task event emission for participant joined - just data update
1518
+ });
1519
+
1520
+ it('should call updateTaskData only once for PARTICIPANT_JOINED_CONFERENCE with pre-calculated isConferenceInProgress', () => {
1521
+ const payload = {
1522
+ data: {
1523
+ type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
1524
+ interactionId: taskId,
1525
+ participantId: 'new-agent-789',
1526
+ interaction: {
1527
+ participants: {
1528
+ [agentId]: { pType: 'Agent', hasLeft: false },
1529
+ 'agent-2': { pType: 'Agent', hasLeft: false },
1530
+ 'new-agent-789': { pType: 'Agent', hasLeft: false },
1531
+ 'customer-1': { pType: 'Customer', hasLeft: false },
1532
+ },
1533
+ media: {
1534
+ [taskId]: {
1535
+ mType: 'mainCall',
1536
+ participants: [agentId, 'agent-2', 'new-agent-789', 'customer-1'],
1537
+ },
1538
+ },
1539
+ },
1540
+ },
1541
+ };
1542
+
1543
+ const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
1544
+
1545
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1546
+
1547
+ // Verify updateTaskData was called exactly once
1548
+ expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
1549
+
1550
+ // Verify it was called with isConferenceInProgress already calculated
1551
+ expect(updateTaskDataSpy).toHaveBeenCalledWith(
1552
+ expect.objectContaining({
1553
+ participantId: 'new-agent-789',
1554
+ isConferenceInProgress: true, // 3 active agents
1555
+ })
1556
+ );
1557
+
1558
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
1559
+ });
1560
+
1561
+ describe('PARTICIPANT_LEFT_CONFERENCE event handling', () => {
1562
+ it('should call updateTaskData only once for PARTICIPANT_LEFT_CONFERENCE with pre-calculated isConferenceInProgress', () => {
1563
+ const payload = {
1564
+ data: {
1565
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1566
+ interactionId: taskId,
1567
+ interaction: {
1568
+ participants: {
1569
+ [agentId]: { pType: 'Agent', hasLeft: false },
1570
+ 'agent-2': { pType: 'Agent', hasLeft: true }, // This agent left
1571
+ 'customer-1': { pType: 'Customer', hasLeft: false },
1572
+ },
1573
+ media: {
1574
+ [taskId]: {
1575
+ mType: 'mainCall',
1576
+ participants: [agentId, 'customer-1'], // agent-2 removed from participants
1577
+ },
1578
+ },
1579
+ },
1580
+ },
1581
+ };
1582
+
1583
+ const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
1584
+
1585
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1586
+
1587
+ // Verify updateTaskData was called exactly once
1588
+ expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
1589
+
1590
+ // Verify it was called with isConferenceInProgress already calculated
1591
+ expect(updateTaskDataSpy).toHaveBeenCalledWith(
1592
+ expect.objectContaining({
1593
+ isConferenceInProgress: false, // Only 1 active agent remains
1594
+ })
1595
+ );
1596
+
1597
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1598
+ });
1599
+
1600
+ it('should emit TASK_PARTICIPANT_LEFT event when participant leaves conference', () => {
1601
+ const payload = {
1602
+ data: {
1603
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1604
+ interactionId: taskId,
1605
+ interaction: {
1606
+ participants: {
1607
+ [agentId]: {
1608
+ hasLeft: false,
1609
+ },
1610
+ },
1611
+ },
1612
+ },
1613
+ };
1614
+
1615
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1616
+
1617
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1618
+ });
1619
+
1620
+ it('should NOT remove task when agent is still in interaction', () => {
1621
+ const payload = {
1622
+ data: {
1623
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1624
+ interactionId: taskId,
1625
+ interaction: {
1626
+ participants: {
1627
+ [agentId]: {
1628
+ hasLeft: false,
1629
+ },
1630
+ },
1631
+ },
1632
+ },
1633
+ };
1634
+
1635
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1636
+
1637
+ // Task should still exist in collection
1638
+ expect(taskManager.getTask(taskId)).toBeDefined();
1639
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1640
+ });
1641
+
1642
+ it('should NOT remove task when agent left but is in main interaction', () => {
1643
+ const payload = {
1644
+ data: {
1645
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1646
+ interactionId: taskId,
1647
+ interaction: {
1648
+ participants: {
1649
+ [agentId]: {
1650
+ hasLeft: true,
1651
+ },
1652
+ },
1653
+ media: {
1654
+ [taskId]: {
1655
+ mType: 'mainCall',
1656
+ participants: [agentId],
1657
+ },
1658
+ },
1659
+ },
1660
+ },
1661
+ };
1662
+
1663
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1664
+
1665
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1666
+
1667
+ // Task should still exist - not removed
1668
+ expect(removeTaskSpy).not.toHaveBeenCalled();
1669
+ expect(taskManager.getTask(taskId)).toBeDefined();
1670
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1671
+ });
1672
+
1673
+ it('should NOT remove task when agent left but is primary (owner)', () => {
1674
+ const payload = {
1675
+ data: {
1676
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1677
+ interactionId: taskId,
1678
+ interaction: {
1679
+ participants: {
1680
+ [agentId]: {
1681
+ hasLeft: true,
1682
+ },
1683
+ },
1684
+ owner: agentId,
1685
+ media: {
1686
+ [taskId]: {
1687
+ mType: 'consultCall',
1688
+ participants: ['other-agent'],
1689
+ },
1690
+ },
1691
+ },
1692
+ },
1693
+ };
1694
+
1695
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1696
+
1697
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1698
+
1699
+ // Task should still exist - not removed because agent is primary
1700
+ expect(removeTaskSpy).not.toHaveBeenCalled();
1701
+ expect(taskManager.getTask(taskId)).toBeDefined();
1702
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1703
+ });
1704
+
1705
+ it('should remove task when agent left and is NOT in main interaction and is NOT primary', () => {
1706
+ const payload = {
1707
+ data: {
1708
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1709
+ interactionId: taskId,
1710
+ interaction: {
1711
+ participants: {
1712
+ [agentId]: {
1713
+ hasLeft: true,
1714
+ },
1715
+ },
1716
+ owner: 'another-agent-id',
1717
+ media: {
1718
+ [taskId]: {
1719
+ mType: 'mainCall',
1720
+ participants: ['another-agent-id'],
1721
+ },
1722
+ },
1723
+ },
1724
+ },
1725
+ };
1726
+
1727
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1728
+
1729
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1730
+
1731
+ // Task should be removed
1732
+ expect(removeTaskSpy).toHaveBeenCalled();
1733
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1734
+ });
1735
+
1736
+ it('should remove task when agent is not in participants list', () => {
1737
+ const payload = {
1738
+ data: {
1739
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1740
+ interactionId: taskId,
1741
+ interaction: {
1742
+ participants: {
1743
+ 'other-agent-id': {
1744
+ hasLeft: false,
1745
+ },
1746
+ },
1747
+ owner: 'another-agent-id',
1748
+ },
1749
+ },
1750
+ };
1751
+
1752
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1753
+
1754
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1755
+
1756
+ // Task should be removed because agent is not in participants
1757
+ expect(removeTaskSpy).toHaveBeenCalled();
1758
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1759
+ });
1760
+
1761
+ it('should update isConferenceInProgress based on remaining active agents', () => {
1762
+ const payload = {
1763
+ data: {
1764
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1765
+ interactionId: taskId,
1766
+ interaction: {
1767
+ participants: {
1768
+ [agentId]: {
1769
+ hasLeft: false,
1770
+ pType: 'Agent',
1771
+ },
1772
+ 'agent-2': {
1773
+ hasLeft: false,
1774
+ pType: 'Agent',
1775
+ },
1776
+ 'customer-1': {
1777
+ hasLeft: false,
1778
+ pType: 'Customer',
1779
+ },
1780
+ },
1781
+ media: {
1782
+ [taskId]: {
1783
+ mType: 'mainCall',
1784
+ participants: [agentId, 'agent-2', 'customer-1'],
1785
+ },
1786
+ },
1787
+ },
1788
+ },
1789
+ };
1790
+
1791
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1792
+
1793
+ // isConferenceInProgress should be true (2 active agents)
1794
+ expect(task.data.isConferenceInProgress).toBe(true);
1795
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1796
+ });
1797
+
1798
+ it('should set isConferenceInProgress to false when only one agent remains', () => {
1799
+ const payload = {
1800
+ data: {
1801
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1802
+ interactionId: taskId,
1803
+ interaction: {
1804
+ participants: {
1805
+ [agentId]: {
1806
+ hasLeft: false,
1807
+ pType: 'Agent',
1808
+ },
1809
+ 'agent-2': {
1810
+ hasLeft: true,
1811
+ pType: 'Agent',
1812
+ },
1813
+ 'customer-1': {
1814
+ hasLeft: false,
1815
+ pType: 'Customer',
1816
+ },
1817
+ },
1818
+ media: {
1819
+ [taskId]: {
1820
+ mType: 'mainCall',
1821
+ participants: [agentId, 'customer-1'],
1822
+ },
1823
+ },
1824
+ },
1825
+ },
1826
+ };
1827
+
1828
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1829
+
1830
+ // isConferenceInProgress should be false (only 1 active agent)
1831
+ expect(task.data.isConferenceInProgress).toBe(false);
1832
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1833
+ });
1834
+
1835
+ it('should handle participant left when no participants data exists', () => {
1836
+ const payload = {
1837
+ data: {
1838
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1839
+ interactionId: taskId,
1840
+ interaction: {},
1841
+ },
1842
+ };
1843
+
1844
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1845
+
1846
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1847
+
1848
+ // When no participants data exists, checkParticipantNotInInteraction returns true
1849
+ // Since agent won't be in main interaction either, task should be removed
1850
+ expect(removeTaskSpy).toHaveBeenCalled();
1851
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1852
+ });
1853
+ });
1854
+
1855
+ it('should handle PARTICIPANT_LEFT_CONFERENCE_FAILED event', () => {
1856
+ const payload = {
1857
+ data: {
1858
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED,
1859
+ interactionId: taskId,
1860
+ reason: 'Exit failed',
1861
+ },
1862
+ };
1863
+
1864
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1865
+
1866
+ expect(task.data.reason).toBe('Exit failed');
1867
+ // No event emission expected for failure - handled by contact method promise rejection
1868
+ });
1869
+
1870
+ it('should only update task for matching interactionId', () => {
1871
+ const otherTaskId = 'other-task-id';
1872
+ const otherTask = {
1873
+ data: { interactionId: otherTaskId },
1874
+ emit: jest.fn(),
1875
+ };
1876
+ taskManager.taskCollection[otherTaskId] = otherTask;
1877
+
1878
+ const payload = {
1879
+ data: {
1880
+ type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
1881
+ interactionId: taskId,
1882
+ isConferencing: true,
1883
+ },
1884
+ };
1885
+
1886
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1887
+
1888
+ // Only the matching task should be updated
1889
+ expect(task.data.isConferencing).toBe(true);
1890
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
1891
+
1892
+ // Other task should not be affected
1893
+ expect(otherTask.data.isConferencing).toBeUndefined();
1894
+ expect(otherTask.emit).not.toHaveBeenCalled();
1895
+ });
1896
+ });
1897
+
1898
+ describe('CONTACT_MERGED event handling', () => {
1899
+ let task;
1900
+ let taskEmitSpy;
1901
+ let managerEmitSpy;
1902
+
1903
+ beforeEach(() => {
1904
+ // Create initial task
1905
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1906
+ task = taskManager.getTask(taskId);
1907
+ taskEmitSpy = jest.spyOn(task, 'emit');
1908
+ managerEmitSpy = jest.spyOn(taskManager, 'emit');
1909
+ });
1910
+
1911
+ it('should update existing task data and emit TASK_MERGED event when CONTACT_MERGED is received', () => {
1912
+ const mergedPayload = {
1913
+ data: {
1914
+ type: CC_EVENTS.CONTACT_MERGED,
1915
+ interactionId: taskId,
1916
+ agentId: taskDataMock.agentId,
1917
+ interaction: {
1918
+ ...taskDataMock.interaction,
1919
+ state: 'merged',
1920
+ customField: 'updated-value',
1921
+ },
1922
+ },
1923
+ };
1924
+
1925
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
1926
+
1927
+ const updatedTask = taskManager.getTask(taskId);
1928
+ expect(updatedTask).toBeDefined();
1929
+ expect(updatedTask.data.interaction.customField).toBe('updated-value');
1930
+ expect(updatedTask.data.interaction.state).toBe('merged');
1931
+ expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, updatedTask);
1932
+ });
1933
+
1934
+ it('should create new task when CONTACT_MERGED is received for non-existing task', () => {
1935
+ const newMergedTaskId = 'new-merged-task-id';
1936
+ const mergedPayload = {
1937
+ data: {
1938
+ type: CC_EVENTS.CONTACT_MERGED,
1939
+ interactionId: newMergedTaskId,
1940
+ agentId: taskDataMock.agentId,
1941
+ interaction: {
1942
+ mediaType: 'telephony',
1943
+ state: 'merged',
1944
+ participants: {
1945
+ [taskDataMock.agentId]: {
1946
+ isWrapUp: false,
1947
+ hasJoined: true,
1948
+ },
1949
+ },
1950
+ },
1951
+ },
1952
+ };
1953
+
1954
+ // Verify task doesn't exist before
1955
+ expect(taskManager.getTask(newMergedTaskId)).toBeUndefined();
1956
+
1957
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
1958
+
1959
+ // Verify task was created
1960
+ const newTask = taskManager.getTask(newMergedTaskId);
1961
+ expect(newTask).toBeDefined();
1962
+ expect(newTask.data.interactionId).toBe(newMergedTaskId);
1963
+ expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, newTask);
1964
+ });
1965
+
1966
+ it('should remove child task when childInteractionId is present in CONTACT_MERGED', () => {
1967
+ const childTaskId = 'child-task-id';
1968
+ const parentTaskId = 'parent-task-id';
1969
+
1970
+ // Create child task
1971
+ const childPayload = {
1972
+ data: {
1973
+ type: CC_EVENTS.AGENT_CONTACT_RESERVED,
1974
+ interactionId: childTaskId,
1975
+ agentId: taskDataMock.agentId,
1976
+ interaction: {mediaType: 'telephony'},
1977
+ },
1978
+ };
1979
+ webSocketManagerMock.emit('message', JSON.stringify(childPayload));
1980
+
1981
+ // Verify child task exists
1982
+ expect(taskManager.getTask(childTaskId)).toBeDefined();
1983
+
1984
+ // Create parent task
1985
+ const parentPayload = {
1986
+ data: {
1987
+ type: CC_EVENTS.AGENT_CONTACT_RESERVED,
1988
+ interactionId: parentTaskId,
1989
+ agentId: taskDataMock.agentId,
1990
+ interaction: {mediaType: 'telephony'},
1991
+ },
1992
+ };
1993
+ webSocketManagerMock.emit('message', JSON.stringify(parentPayload));
1994
+
1995
+ // Send CONTACT_MERGED with childInteractionId
1996
+ const mergedPayload = {
1997
+ data: {
1998
+ type: CC_EVENTS.CONTACT_MERGED,
1999
+ interactionId: parentTaskId,
2000
+ childInteractionId: childTaskId,
2001
+ agentId: taskDataMock.agentId,
2002
+ interaction: {
2003
+ mediaType: 'telephony',
2004
+ state: 'merged',
2005
+ },
2006
+ },
2007
+ };
2008
+
2009
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2010
+
2011
+ // Verify child task was removed
2012
+ expect(taskManager.getTask(childTaskId)).toBeUndefined();
2013
+
2014
+ // Verify parent task still exists
2015
+ expect(taskManager.getTask(parentTaskId)).toBeDefined();
2016
+
2017
+ // Verify TASK_MERGED event was emitted
2018
+ expect(managerEmitSpy).toHaveBeenCalledWith(
2019
+ TASK_EVENTS.TASK_MERGED,
2020
+ expect.objectContaining({
2021
+ data: expect.objectContaining({
2022
+ interactionId: parentTaskId,
2023
+ }),
2024
+ })
2025
+ );
2026
+ });
2027
+
2028
+ it('should handle CONTACT_MERGED with EP-DN participant correctly', () => {
2029
+ const epdnTaskId = 'epdn-merged-task';
2030
+ const mergedPayload = {
2031
+ data: {
2032
+ type: CC_EVENTS.CONTACT_MERGED,
2033
+ interactionId: epdnTaskId,
2034
+ agentId: taskDataMock.agentId,
2035
+ interaction: {
2036
+ mediaType: 'telephony',
2037
+ state: 'merged',
2038
+ participants: {
2039
+ [taskDataMock.agentId]: {
2040
+ type: 'Agent',
2041
+ isWrapUp: false,
2042
+ hasJoined: true,
2043
+ },
2044
+ 'epdn-participant': {
2045
+ type: 'EpDn',
2046
+ epId: 'entry-point-123',
2047
+ isWrapUp: false,
2048
+ },
2049
+ },
2050
+ },
2051
+ },
2052
+ };
2053
+
2054
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2055
+
2056
+ const mergedTask = taskManager.getTask(epdnTaskId);
2057
+ expect(mergedTask).toBeDefined();
2058
+ expect(mergedTask.data.interaction.participants['epdn-participant']).toBeDefined();
2059
+ expect(mergedTask.data.interaction.participants['epdn-participant'].type).toBe('EpDn');
2060
+ expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, mergedTask);
2061
+ });
2062
+
2063
+ it('should not affect other tasks when CONTACT_MERGED is received', () => {
2064
+ const otherTaskId = 'other-unrelated-task';
2065
+ const otherPayload = {
2066
+ data: {
2067
+ type: CC_EVENTS.AGENT_CONTACT_RESERVED,
2068
+ interactionId: otherTaskId,
2069
+ agentId: taskDataMock.agentId,
2070
+ interaction: {mediaType: 'chat'},
2071
+ },
2072
+ };
2073
+ webSocketManagerMock.emit('message', JSON.stringify(otherPayload));
2074
+
2075
+ const otherTask = taskManager.getTask(otherTaskId);
2076
+ const otherTaskEmitSpy = jest.spyOn(otherTask, 'emit');
2077
+
2078
+ // Send CONTACT_MERGED for the original task
2079
+ const mergedPayload = {
2080
+ data: {
2081
+ type: CC_EVENTS.CONTACT_MERGED,
2082
+ interactionId: taskId,
2083
+ agentId: taskDataMock.agentId,
2084
+ interaction: {
2085
+ mediaType: 'telephony',
2086
+ state: 'merged',
2087
+ },
2088
+ },
2089
+ };
2090
+
2091
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2092
+
2093
+ // Verify other task was not affected
2094
+ expect(otherTaskEmitSpy).not.toHaveBeenCalled();
2095
+ expect(otherTask.data.interaction.mediaType).toBe('chat');
2096
+
2097
+ // Verify original task was updated
2098
+ expect(managerEmitSpy).toHaveBeenCalledWith(
2099
+ TASK_EVENTS.TASK_MERGED,
2100
+ expect.objectContaining({
2101
+ data: expect.objectContaining({
2102
+ interactionId: taskId,
2103
+ }),
2104
+ })
2105
+ );
2106
+ });
2107
+ });
1350
2108
  });
1351
2109